xuym-codex-collab 0.9.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/apps/onboarding/main.mjs +46 -3
- package/apps/onboarding/server.mjs +16 -3
- package/apps/onboarding/web/app.js +1 -1
- package/apps/onboarding/web/effects.css +47 -0
- package/apps/onboarding/web/effects.js +323 -0
- package/apps/onboarding/web/index.html +13 -7
- package/apps/onboarding/web/style.css +2 -7
- package/docs/releases/0.10.0.md +20 -0
- package/docs/releases/0.10.1.md +16 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ codex-collab setup
|
|
|
15
15
|
|
|
16
16
|
安装后,先运行 `codex-collab setup`。已有业务项目时,进入项目目录再运行可自动带入信息;没有本机 clone 时可让向导拉取。以后直接运行 `codex-collab` 也会打开向导。上面的 `--foreground-scripts` 会让 npm 显示安装完成后的下一步提示。安装过程不会自动打开浏览器或修改项目;未使用此参数时 npm 可能隐藏提示,使用 `--ignore-scripts` 时会跳过提示,都可以直接运行 `codex-collab setup` 开始。
|
|
17
17
|
|
|
18
|
+
升级 npm 包后重新运行 `codex-collab setup`,向导会检查后台服务版本,自动切换到本次安装的版本并打开新链接。同版本重复打开会复用现有服务,已有项目配置、对话绑定和独立运行的机器人连接保留。
|
|
19
|
+
|
|
18
20
|
向导每屏只处理一个动作。管理员选择“接入一个项目”,确认目录和自动识别的分支、验证命令,再填写 Bot ID/Secret;成员选择“接受同事的邀请”,读取接入包后确认项目目录。群身份验证、等待审批和本机配置会自动衔接。最后复制指令到指定 Codex 对话,页面依据实际绑定和轮询记录显示完成。已在使用旧配置的成员可在首页“其他接入方式”中选择“导入已有配置”。命令启动后台工作台后退出,可以关闭终端和浏览器。macOS 默认设置登录自启;Linux 尝试配置 systemd 用户服务,失败时明确提示。工作台每 15 秒检查一次已开启的机器人进程,意外退出时恢复并在失败后退避重试;手动停止的连接保持停止。
|
|
19
21
|
|
|
20
22
|
| 谁 | 在工作台做什么 | 接入后需要保留什么 |
|
|
@@ -26,7 +28,7 @@ codex-collab setup
|
|
|
26
28
|
|
|
27
29
|
协作远程与业务基准分支自动识别;不要求 `origin/main`。有多个不同远程或默认分支不明确时,管理员选择一次。成员与新增机器人继承签名团队基准,各自可使用不同远程别名。每个新任务固定创建时的 commit,排队和重试期间不会漂移;管理员可在团队设置里为后续任务修改基准分支。
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
工作台首页按项目汇总,同一仓库的多个本机机器人共用一张项目卡片。打开项目后,所有团队机器人展示在同一列表里,自己的标记“我的”;本机已接入的机器人可继续配置或管理,其他机器人的职责来自签名团队记录。分支、诊断、重置和团队分配设置按需展开。首页以“让 Codex 组队工作”为入口:在企业微信群里连接你和同事的 Codex 会话,按角色分工、接力完成任务。界面提供纸白、夜色两种配色,以及从本机加载的 Three.js 星尘背景、轻微鼠标视差与短尾迹;触屏和系统减少动态效果设置下显示静态星点,页面进入后台时暂停动画。
|
|
30
32
|
|
|
31
33
|
同一个真人可以拥有多个 Agent,每个 Agent 使用独立机器人、签名身份、业务 clone 和 Codex 对话。管理员在项目页点击“添加我的机器人”,验证同一个企业微信账号后自动接入,无需下载包或填写包密码;给同事分配时仍使用加密接入包,同事可以领取多份。相同 userid 不会合并 Agent 权限,审核通知会指明应使用的机器人。
|
|
32
34
|
|
package/apps/onboarding/main.mjs
CHANGED
|
@@ -8,8 +8,11 @@ import { SetupManager, savePrivate } from './manager.mjs';
|
|
|
8
8
|
import { RunnerSupervisor } from './supervisor.mjs';
|
|
9
9
|
import { workspaceHome } from './workspace.mjs';
|
|
10
10
|
import { configureStartup, startupStatus } from './startup.mjs';
|
|
11
|
-
import { alive } from '../peer/process.mjs';
|
|
11
|
+
import { alive, acquireRunnerControl } from '../peer/process.mjs';
|
|
12
12
|
const entry = fileURLToPath(import.meta.url);
|
|
13
|
+
const { version } = JSON.parse(await readFile(new URL('../../package.json', import.meta.url), 'utf8'));
|
|
14
|
+
const runtime = { version, entry, node: process.execPath };
|
|
15
|
+
const sameRuntime = (state) => Object.entries(runtime).every(([key, value]) => state.runtime?.[key] === value);
|
|
13
16
|
function openBrowser(url) {
|
|
14
17
|
const command = process.platform === 'darwin' ? ['open', url] : process.platform === 'win32' ? ['cmd', '/c', 'start', '', url] : ['xdg-open', url];
|
|
15
18
|
const child = spawn(command[0], command.slice(1), { stdio: 'ignore' }); child.on('error', () => {}); child.unref();
|
|
@@ -38,7 +41,7 @@ export async function runService({ home = workspaceHome() } = {}) {
|
|
|
38
41
|
try {
|
|
39
42
|
const manager = new SetupManager({ home });
|
|
40
43
|
app = await startSetupServer({ manager });
|
|
41
|
-
await lock.writeFile(JSON.stringify({ pid: process.pid, url: app.url, started_at: new Date().toISOString() }) + '\n'); await lock.close();
|
|
44
|
+
await lock.writeFile(JSON.stringify({ pid: process.pid, url: app.url, started_at: new Date().toISOString(), runtime }) + '\n'); await lock.close();
|
|
42
45
|
const supervisor = new RunnerSupervisor(manager);
|
|
43
46
|
const tick = async () => {
|
|
44
47
|
try { await manager.exclusive(() => supervisor.tick()); } catch { /* Local diagnostics show missing/invalid profiles without logging secrets. */ }
|
|
@@ -53,9 +56,49 @@ export async function runService({ home = workspaceHome() } = {}) {
|
|
|
53
56
|
return { ...app, stop };
|
|
54
57
|
} catch (error) { await lock.close().catch(() => {}); await rm(path, { force: true }); if (app) await app.close(); throw error; }
|
|
55
58
|
}
|
|
59
|
+
async function stopOutdatedService(home, state) {
|
|
60
|
+
// A private lock can outlive its process. Authenticate the loopback service
|
|
61
|
+
// before signalling a legacy PID; never terminate an unrelated process.
|
|
62
|
+
let verified = false;
|
|
63
|
+
try {
|
|
64
|
+
const url = new URL(state.url);
|
|
65
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password || !/^#[a-f0-9]{64}$/.test(url.hash)) throw new Error();
|
|
66
|
+
const response = await fetch(`${url.origin}/api/session`, {
|
|
67
|
+
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(5000),
|
|
68
|
+
headers: { Origin: url.origin, 'Content-Type': 'application/json' },
|
|
69
|
+
body: JSON.stringify({ token: url.hash.slice(1) }),
|
|
70
|
+
});
|
|
71
|
+
verified = response.ok && (await response.json()).ok === true;
|
|
72
|
+
} catch { /* Keep private service URLs and network errors out of diagnostics. */ }
|
|
73
|
+
if (!verified) throw new Error('无法确认旧版向导服务的身份,请检查后台服务后重试。');
|
|
74
|
+
const current = await serviceState(home);
|
|
75
|
+
if (!current || current.pid !== state.pid || current.url !== state.url) return;
|
|
76
|
+
try { process.kill(state.pid, 'SIGTERM'); }
|
|
77
|
+
catch (error) { if (error.code !== 'ESRCH') throw error; }
|
|
78
|
+
for (let attempt = 0; attempt < 150; attempt++) {
|
|
79
|
+
if (!alive(state.pid)) return;
|
|
80
|
+
await delay(100);
|
|
81
|
+
}
|
|
82
|
+
throw new Error('旧版向导服务尚未退出,请稍后重新运行 codex-collab setup。');
|
|
83
|
+
}
|
|
56
84
|
export async function runLocalSetup({ open = true, home = workspaceHome(), cwd = process.cwd(), foreground = false, startup = true } = {}) {
|
|
57
|
-
home = resolve(home); await
|
|
85
|
+
home = resolve(home); await mkdir(home, { recursive: true, mode: 0o700 });
|
|
86
|
+
// Serialize launchers so two setup commands cannot both stop a service that
|
|
87
|
+
// another launcher has just upgraded. Runner processes keep their own locks.
|
|
88
|
+
const release = await acquireRunnerControl(join(home, 'launcher'), entry);
|
|
89
|
+
try {
|
|
90
|
+
await savePrivate(join(home, 'context.json'), { cwd: resolve(cwd) });
|
|
91
|
+
return await launchLocalSetup({ open, home, foreground, startup });
|
|
92
|
+
} finally { await release(); }
|
|
93
|
+
}
|
|
94
|
+
async function launchLocalSetup({ open, home, foreground, startup }) {
|
|
58
95
|
let state = await serviceState(home);
|
|
96
|
+
if (state && !sameRuntime(state)) {
|
|
97
|
+
await stopOutdatedService(home, state);
|
|
98
|
+
state = await serviceState(home);
|
|
99
|
+
if (state && !sameRuntime(state)) throw new Error('向导服务正在更新,请稍后重新运行 codex-collab setup。');
|
|
100
|
+
console.log('已更新本机向导服务。');
|
|
101
|
+
}
|
|
59
102
|
if (foreground) {
|
|
60
103
|
if (state) { console.log(`本机工作台已运行:${state.url}`); if (open) openBrowser(state.url); return; }
|
|
61
104
|
const app = await runService({ home }); if (app) { console.log(`Codex Collab 工作台:${app.url}`); if (open) openBrowser(app.url); } return app;
|
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
|
-
import {
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
5
6
|
import { SetupError } from './errors.mjs';
|
|
6
7
|
import { SetupManager } from './manager.mjs';
|
|
7
|
-
const
|
|
8
|
+
const threeBuild = new URL('.', pathToFileURL(createRequire(import.meta.url).resolve('three')));
|
|
9
|
+
const assets = {
|
|
10
|
+
'/': ['index.html', 'text/html; charset=utf-8'],
|
|
11
|
+
'/favicon.svg': ['favicon.svg', 'image/svg+xml'],
|
|
12
|
+
'/app.js': ['app.js', 'text/javascript; charset=utf-8'],
|
|
13
|
+
'/style.css': ['style.css', 'text/css; charset=utf-8'],
|
|
14
|
+
'/effects.js': ['effects.js', 'text/javascript; charset=utf-8'],
|
|
15
|
+
'/effects.css': ['effects.css', 'text/css; charset=utf-8'],
|
|
16
|
+
'/vendor/three.module.js': [new URL('three.module.js', threeBuild), 'text/javascript; charset=utf-8'],
|
|
17
|
+
'/vendor/three.core.js': [new URL('three.core.js', threeBuild), 'text/javascript; charset=utf-8'],
|
|
18
|
+
};
|
|
8
19
|
const same = (a, b) => typeof a === 'string' && Buffer.byteLength(a) === Buffer.byteLength(b) && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
9
20
|
export async function startSetupServer({ manager = new SetupManager(), port = 0 } = {}) {
|
|
10
21
|
const token = randomBytes(32).toString('hex'); const cookieName = `collab_${randomBytes(8).toString('hex')}`; let origin;
|
|
@@ -16,7 +27,9 @@ export async function startSetupServer({ manager = new SetupManager(), port = 0
|
|
|
16
27
|
if (req.headers.host !== new URL(origin).host) return json(403, { error: '本机访问地址无效。' });
|
|
17
28
|
const url = new URL(req.url, origin);
|
|
18
29
|
if (req.method === 'GET' && assets[url.pathname]) {
|
|
19
|
-
const [file, type] = assets[url.pathname];
|
|
30
|
+
const [file, type] = assets[url.pathname];
|
|
31
|
+
const content = await readFile(file instanceof URL ? file : new URL(`./web/${file}`, import.meta.url));
|
|
32
|
+
res.writeHead(200, { 'Content-Type': type }); res.end(content); return;
|
|
20
33
|
}
|
|
21
34
|
if (req.method !== 'GET' && (req.headers.origin !== origin || req.headers['content-type']?.split(';')[0] !== 'application/json')) return json(403, { error: '请通过本机向导操作。' });
|
|
22
35
|
if (req.headers.origin && req.headers.origin !== origin) return json(403, { error: '访问来源无效。' });
|
|
@@ -606,7 +606,7 @@ $('pending-requests').onclick = () => { $('admin-tools').open = true; $('request
|
|
|
606
606
|
function initializeAppearance() {
|
|
607
607
|
if (!document.body) return;
|
|
608
608
|
function apply(theme) {
|
|
609
|
-
if (!['day', '
|
|
609
|
+
if (!['day', 'night'].includes(theme)) theme = 'day';
|
|
610
610
|
document.body.dataset.theme = theme;
|
|
611
611
|
for (const choice of document.querySelectorAll('[data-theme-choice]')) choice.setAttribute('aria-pressed', String(choice.dataset.themeChoice === theme));
|
|
612
612
|
try { localStorage.setItem('codex-collab-theme', theme); } catch {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/* Three.js star field. Content stays on its own layer with native hit testing. */
|
|
2
|
+
body {
|
|
3
|
+
isolation: isolate;
|
|
4
|
+
--fx-blue: 158 181 222;
|
|
5
|
+
--fx-peach: 237 207 175;
|
|
6
|
+
--fx-line: 102 142 133;
|
|
7
|
+
--fx-cursor: 145 133 187;
|
|
8
|
+
}
|
|
9
|
+
body[data-theme="night"] {
|
|
10
|
+
--fx-blue: 87 111 177;
|
|
11
|
+
--fx-peach: 141 119 83;
|
|
12
|
+
--fx-line: 139 186 176;
|
|
13
|
+
--fx-cursor: 198 186 232;
|
|
14
|
+
}
|
|
15
|
+
#ambient { position: fixed; inset: 0; overflow: hidden; pointer-events: none; z-index: -1; contain: strict; }
|
|
16
|
+
.ambient-light { position: absolute; width: 82vw; height: 88vh; pointer-events: none; will-change: transform; }
|
|
17
|
+
.light-one { left: -31vw; top: -13vh; background: radial-gradient(ellipse, rgb(var(--fx-blue) / .24), transparent 65%); animation: drift-one 24s ease-in-out infinite alternate; }
|
|
18
|
+
.light-two { right: -31vw; top: 1vh; background: radial-gradient(ellipse, rgb(170 153 202 / .19), transparent 68%); animation: drift-two 29s ease-in-out infinite alternate; }
|
|
19
|
+
.light-three { width: 100vw; height: 65vh; left: -6vw; bottom: -33vh; opacity: .28; background: radial-gradient(ellipse, rgb(var(--fx-peach) / .43), transparent 69%); animation: drift-three 31s ease-in-out infinite alternate; }
|
|
20
|
+
body[data-theme="night"] .light-one { opacity: .72; }
|
|
21
|
+
body[data-theme="night"] .light-two { opacity: .8; }
|
|
22
|
+
html:not([data-webgl="ready"]) #ambient::after { content: ""; position: absolute; left: 0; top: 0; width: 2px; height: 2px; border-radius: 50%; color: rgb(var(--fx-line) / .52); box-shadow: 9vw 18vh, 24vw 9vh, 37vw 28vh, 68vw 13vh, 88vw 24vh, 14vw 47vh, 31vw 66vh, 75vw 55vh, 91vw 68vh, 5vw 79vh, 22vw 86vh, 42vw 91vh, 62vw 81vh, 81vw 92vh, 96vw 43vh, 54vw 7vh, 72vw 34vh, 17vw 32vh, 85vw 8vh; }
|
|
23
|
+
#ambient-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
24
|
+
#cursor-follower { position: fixed; top: 0; left: 0; width: 30px; height: 30px; border: 1px solid rgb(var(--fx-cursor) / .35); border-radius: 50%; pointer-events: none; opacity: 0; z-index: 5; will-change: transform; transition: width 180ms, height 180ms, border-color 180ms; }
|
|
25
|
+
#cursor-follower i { display: block; position: absolute; width: 4px; height: 4px; background: rgb(var(--fx-cursor) / .6); border-radius: 50%; right: -2px; top: 12px; }
|
|
26
|
+
.masthead, main, .demo-footer { position: relative; z-index: 1; }
|
|
27
|
+
.effect-note { display: flex; align-items: center; gap: 8px; color: var(--muted); min-width: 0; font-size: 10px; }
|
|
28
|
+
.demo-only { font-size: 8px; opacity: .6; }
|
|
29
|
+
html[data-effects-paused="true"] .ambient-light { animation-play-state: paused; }
|
|
30
|
+
html[data-effects-static="true"] #cursor-follower { visibility: hidden; }
|
|
31
|
+
@keyframes drift-one { from { transform: translate3d(0, 0, 0) rotate(-12deg); } to { transform: translate3d(13vw, 8vh, 0) rotate(14deg); } }
|
|
32
|
+
@keyframes drift-two { from { transform: translate3d(0, 0, 0) rotate(8deg); } to { transform: translate3d(-10vw, 13vh, 0) rotate(-15deg); } }
|
|
33
|
+
@keyframes drift-three { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(12vw, -6vh, 0) scale(1.08); } }
|
|
34
|
+
@media (max-width: 600px) {
|
|
35
|
+
.masthead { gap: 10px; align-items: center; }
|
|
36
|
+
.effect-note { font-size: 9px; flex-wrap: wrap; justify-content: center; gap: 5px; }
|
|
37
|
+
.demo-only { flex-basis: 100%; text-align: center; margin: 0; }
|
|
38
|
+
.ambient-light { width: 125vw; }
|
|
39
|
+
}
|
|
40
|
+
@media (hover: none), (pointer: coarse) {
|
|
41
|
+
#cursor-follower { display: none; }
|
|
42
|
+
.ambient-light { animation: none; will-change: auto; }
|
|
43
|
+
}
|
|
44
|
+
@media (prefers-reduced-motion: reduce) {
|
|
45
|
+
.ambient-light { animation: none; will-change: auto; }
|
|
46
|
+
#cursor-follower { display: none; }
|
|
47
|
+
}
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BufferGeometry, Color, Float32BufferAttribute, NormalBlending,
|
|
3
|
+
PerspectiveCamera, Points, Scene, ShaderMaterial, Vector2, WebGLRenderer,
|
|
4
|
+
} from '/vendor/three.module.js';
|
|
5
|
+
|
|
6
|
+
// Stars and pointer dust each render in a single draw call.
|
|
7
|
+
const canvas = document.getElementById('ambient-canvas');
|
|
8
|
+
const follower = document.getElementById('cursor-follower');
|
|
9
|
+
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
10
|
+
const finePointer = matchMedia('(hover: hover) and (pointer: fine)');
|
|
11
|
+
const palettes = {
|
|
12
|
+
day: ['#728f9e', '#9a8bb4', '#a2b7c5', .88],
|
|
13
|
+
night: ['#8fbdbb', '#b5b0e2', '#c5d6e0', .88],
|
|
14
|
+
};
|
|
15
|
+
const pointer = { x: 0, y: 0, ringX: 0, ringY: 0, fieldX: 0, fieldY: 0, alpha: 0, active: false, hover: false, down: false };
|
|
16
|
+
const trailCount = 44;
|
|
17
|
+
const trailPositions = new Float32Array(trailCount * 3);
|
|
18
|
+
const trailBirths = new Float32Array(trailCount).fill(-100);
|
|
19
|
+
const trailSeeds = new Float32Array(trailCount);
|
|
20
|
+
let width = innerWidth, height = innerHeight, frame = 0, lastFrame = 0, elapsed = 0;
|
|
21
|
+
let renderer, scene, camera, stars, dust, starGeometry, dustGeometry;
|
|
22
|
+
let initialized = false, lost = false, trailIndex = 0, lastTrail = 0, lastTrailX = 0, lastTrailY = 0;
|
|
23
|
+
let density = 1;
|
|
24
|
+
const isStatic = () => reducedMotion.matches || !finePointer.matches;
|
|
25
|
+
const uniforms = {
|
|
26
|
+
uTime: { value: 0 },
|
|
27
|
+
uPixelRatio: { value: 1 },
|
|
28
|
+
uSpan: { value: new Vector2(1, 1) },
|
|
29
|
+
uPointer: { value: new Vector2() },
|
|
30
|
+
uParallax: { value: new Vector2() },
|
|
31
|
+
uPointerAlpha: { value: 0 },
|
|
32
|
+
uColorA: { value: new Color() },
|
|
33
|
+
uColorB: { value: new Color() },
|
|
34
|
+
uColorC: { value: new Color() },
|
|
35
|
+
uOpacity: { value: 1 },
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const starVertex = `
|
|
39
|
+
uniform float uTime;
|
|
40
|
+
uniform float uPixelRatio;
|
|
41
|
+
uniform vec2 uSpan;
|
|
42
|
+
uniform vec2 uPointer;
|
|
43
|
+
uniform vec2 uParallax;
|
|
44
|
+
uniform float uPointerAlpha;
|
|
45
|
+
attribute float aSize;
|
|
46
|
+
attribute float aPhase;
|
|
47
|
+
attribute float aTone;
|
|
48
|
+
varying float vTone;
|
|
49
|
+
varying float vAlpha;
|
|
50
|
+
varying float vSparkle;
|
|
51
|
+
void main() {
|
|
52
|
+
float depth = (8.0 - position.z) / 8.0;
|
|
53
|
+
vec3 p = vec3(position.xy * uSpan * depth, position.z);
|
|
54
|
+
p.x += sin(uTime * .085 + aPhase) * .065;
|
|
55
|
+
p.y += cos(uTime * .065 + aPhase * 1.3) * .045;
|
|
56
|
+
// Farther layers respond less, creating subtle depth without camera sway.
|
|
57
|
+
p.xy += uParallax * (.025 + (position.z + 4.0) * .012);
|
|
58
|
+
vec2 delta = uPointer * depth - p.xy;
|
|
59
|
+
float proximity = exp(-dot(delta, delta) / (1.1 * depth * depth));
|
|
60
|
+
p.xy += delta * proximity * .09 * uPointerAlpha;
|
|
61
|
+
vec4 mv = modelViewMatrix * vec4(p, 1.0);
|
|
62
|
+
vec4 projected = projectionMatrix * mv;
|
|
63
|
+
gl_Position = projected;
|
|
64
|
+
gl_PointSize = clamp(aSize * uPixelRatio * (8.0 / -mv.z), 2.0, 28.0 * uPixelRatio);
|
|
65
|
+
vec2 screen = projected.xy / projected.w;
|
|
66
|
+
float quietCenter = 1.0 - exp(-pow(screen.x * 2.2, 2.0) - pow(screen.y * 1.6, 2.0)) * .45;
|
|
67
|
+
vAlpha = (.65 + sin(uTime * .42 + aPhase) * .16) * quietCenter;
|
|
68
|
+
vTone = aTone;
|
|
69
|
+
vSparkle = step(.83, aTone);
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
const starFragment = `
|
|
73
|
+
uniform vec3 uColorA;
|
|
74
|
+
uniform vec3 uColorB;
|
|
75
|
+
uniform vec3 uColorC;
|
|
76
|
+
uniform float uOpacity;
|
|
77
|
+
varying float vTone;
|
|
78
|
+
varying float vAlpha;
|
|
79
|
+
varying float vSparkle;
|
|
80
|
+
void main() {
|
|
81
|
+
vec2 p = gl_PointCoord * 2.0 - 1.0;
|
|
82
|
+
float radius = dot(p, p);
|
|
83
|
+
if (radius > 1.0) discard;
|
|
84
|
+
float core = exp(-radius * 80.0);
|
|
85
|
+
float halo = exp(-radius * 6.0) * .17;
|
|
86
|
+
float cross = (exp(-abs(p.x) * 65.0) + exp(-abs(p.y) * 65.0)) * exp(-radius * 7.0) * .18 * vSparkle;
|
|
87
|
+
float alpha = (core + halo + cross) * vAlpha * uOpacity;
|
|
88
|
+
vec3 color = mix(uColorA, uColorB, smoothstep(.1, .8, vTone));
|
|
89
|
+
color = mix(color, uColorC, vSparkle * .55);
|
|
90
|
+
gl_FragColor = vec4(color, alpha);
|
|
91
|
+
#include <colorspace_fragment>
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
const dustVertex = `
|
|
95
|
+
uniform float uTime;
|
|
96
|
+
uniform float uPixelRatio;
|
|
97
|
+
attribute float aBirth;
|
|
98
|
+
attribute float aSeed;
|
|
99
|
+
varying float vAlpha;
|
|
100
|
+
void main() {
|
|
101
|
+
float age = uTime - aBirth;
|
|
102
|
+
float life = clamp(1.0 - age / 1.25, 0.0, 1.0);
|
|
103
|
+
vec3 p = position;
|
|
104
|
+
p.x += sin(aSeed * 25.0) * age * .042;
|
|
105
|
+
p.y += age * (.035 + aSeed * .035);
|
|
106
|
+
gl_Position = projectionMatrix * modelViewMatrix * vec4(p, 1.0);
|
|
107
|
+
gl_PointSize = (6.0 + aSeed * 7.0) * uPixelRatio * (.7 + life * .3);
|
|
108
|
+
vAlpha = life * life * .65;
|
|
109
|
+
}
|
|
110
|
+
`;
|
|
111
|
+
const dustFragment = `
|
|
112
|
+
uniform vec3 uColorB;
|
|
113
|
+
uniform vec3 uColorC;
|
|
114
|
+
uniform float uOpacity;
|
|
115
|
+
varying float vAlpha;
|
|
116
|
+
void main() {
|
|
117
|
+
vec2 p = gl_PointCoord * 2.0 - 1.0;
|
|
118
|
+
float radius = dot(p, p);
|
|
119
|
+
if (radius > 1.0) discard;
|
|
120
|
+
float softPoint = exp(-radius * 35.0) + exp(-radius * 5.0) * .12;
|
|
121
|
+
gl_FragColor = vec4(mix(uColorB, uColorC, .4), softPoint * vAlpha * uOpacity);
|
|
122
|
+
#include <colorspace_fragment>
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
|
|
126
|
+
function stop() {
|
|
127
|
+
cancelAnimationFrame(frame);
|
|
128
|
+
frame = 0;
|
|
129
|
+
lastFrame = 0;
|
|
130
|
+
}
|
|
131
|
+
function schedule() {
|
|
132
|
+
if (renderer && !frame && !document.hidden && !isStatic() && !lost) frame = requestAnimationFrame(tick);
|
|
133
|
+
}
|
|
134
|
+
function createScene() {
|
|
135
|
+
if (initialized) return;
|
|
136
|
+
initialized = true;
|
|
137
|
+
try {
|
|
138
|
+
renderer = new WebGLRenderer({ canvas, alpha: true, antialias: false, powerPreference: 'low-power' });
|
|
139
|
+
renderer.setClearColor(0x000000, 0);
|
|
140
|
+
scene = new Scene();
|
|
141
|
+
camera = new PerspectiveCamera(42, width / height, .1, 30);
|
|
142
|
+
camera.position.z = 8;
|
|
143
|
+
starGeometry = new BufferGeometry();
|
|
144
|
+
const positions = [], sizes = [], phases = [], tones = [];
|
|
145
|
+
let seed = 173;
|
|
146
|
+
const random = () => { seed = (seed * 16807) % 2147483647; return (seed - 1) / 2147483646; };
|
|
147
|
+
for (let i = 0; i < 148; i++) {
|
|
148
|
+
positions.push((random() - .5) * 1.18, (random() - .5) * 1.18, random() * 6.5 - 4);
|
|
149
|
+
sizes.push(6 + Math.pow(random(), 2) * 13);
|
|
150
|
+
phases.push(random() * Math.PI * 2);
|
|
151
|
+
tones.push(random());
|
|
152
|
+
}
|
|
153
|
+
starGeometry.setAttribute('position', new Float32BufferAttribute(positions, 3));
|
|
154
|
+
starGeometry.setAttribute('aSize', new Float32BufferAttribute(sizes, 1));
|
|
155
|
+
starGeometry.setAttribute('aPhase', new Float32BufferAttribute(phases, 1));
|
|
156
|
+
starGeometry.setAttribute('aTone', new Float32BufferAttribute(tones, 1));
|
|
157
|
+
const settings = { uniforms, transparent: true, depthTest: false, depthWrite: false, blending: NormalBlending };
|
|
158
|
+
stars = new Points(starGeometry, new ShaderMaterial({ ...settings, vertexShader: starVertex, fragmentShader: starFragment }));
|
|
159
|
+
stars.frustumCulled = false;
|
|
160
|
+
dustGeometry = new BufferGeometry();
|
|
161
|
+
dustGeometry.setAttribute('position', new Float32BufferAttribute(trailPositions, 3));
|
|
162
|
+
dustGeometry.setAttribute('aBirth', new Float32BufferAttribute(trailBirths, 1));
|
|
163
|
+
for (let i = 0; i < trailCount; i++) trailSeeds[i] = random();
|
|
164
|
+
dustGeometry.setAttribute('aSeed', new Float32BufferAttribute(trailSeeds, 1));
|
|
165
|
+
dust = new Points(dustGeometry, new ShaderMaterial({ ...settings, vertexShader: dustVertex, fragmentShader: dustFragment }));
|
|
166
|
+
dust.frustumCulled = false;
|
|
167
|
+
scene.add(stars, dust);
|
|
168
|
+
document.documentElement.dataset.webgl = 'ready';
|
|
169
|
+
resizeRenderer();
|
|
170
|
+
} catch {
|
|
171
|
+
disposeScene();
|
|
172
|
+
// CSS keeps a quiet star field if WebGL is unavailable.
|
|
173
|
+
document.documentElement.dataset.webgl = 'fallback';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function disposeScene() {
|
|
177
|
+
starGeometry?.dispose(); dustGeometry?.dispose();
|
|
178
|
+
stars?.material.dispose(); dust?.material.dispose();
|
|
179
|
+
renderer?.dispose();
|
|
180
|
+
renderer = null;
|
|
181
|
+
}
|
|
182
|
+
function resizeRenderer() {
|
|
183
|
+
if (!renderer) return;
|
|
184
|
+
density = Math.min(devicePixelRatio || 1, 1.5, 2048 / Math.max(width, height));
|
|
185
|
+
renderer.setPixelRatio(density);
|
|
186
|
+
renderer.setSize(width, height, false);
|
|
187
|
+
camera.aspect = width / height;
|
|
188
|
+
camera.updateProjectionMatrix();
|
|
189
|
+
const spanY = 2 * Math.tan(42 * Math.PI / 360) * 8;
|
|
190
|
+
uniforms.uSpan.value.set(spanY * camera.aspect, spanY);
|
|
191
|
+
uniforms.uPixelRatio.value = density;
|
|
192
|
+
starGeometry.setDrawRange(0, Math.min(148, Math.max(65, Math.round(width * height / 9500))));
|
|
193
|
+
}
|
|
194
|
+
function render() {
|
|
195
|
+
if (renderer && !lost) renderer.render(scene, camera);
|
|
196
|
+
}
|
|
197
|
+
function updatePalette() {
|
|
198
|
+
const colors = palettes[document.body.dataset.theme] || palettes.day;
|
|
199
|
+
uniforms.uColorA.value.set(colors[0]);
|
|
200
|
+
uniforms.uColorB.value.set(colors[1]);
|
|
201
|
+
uniforms.uColorC.value.set(colors[2]);
|
|
202
|
+
uniforms.uOpacity.value = colors[3];
|
|
203
|
+
render();
|
|
204
|
+
}
|
|
205
|
+
function clearDust() {
|
|
206
|
+
trailBirths.fill(-100);
|
|
207
|
+
if (dustGeometry) {
|
|
208
|
+
dustGeometry.getAttribute('aBirth').array.set(trailBirths);
|
|
209
|
+
dustGeometry.getAttribute('aBirth').needsUpdate = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function updatePreferences() {
|
|
213
|
+
stop();
|
|
214
|
+
const staticMode = isStatic();
|
|
215
|
+
document.documentElement.dataset.effectsPaused = String(document.hidden);
|
|
216
|
+
document.documentElement.dataset.effectsStatic = String(staticMode);
|
|
217
|
+
// Touch/reduced-motion starts with CSS alone; no WebGL context or render loop.
|
|
218
|
+
if (!staticMode) createScene();
|
|
219
|
+
if (staticMode) {
|
|
220
|
+
pointer.alpha = 0;
|
|
221
|
+
uniforms.uPointerAlpha.value = 0;
|
|
222
|
+
clearDust();
|
|
223
|
+
render();
|
|
224
|
+
}
|
|
225
|
+
updatePalette();
|
|
226
|
+
schedule();
|
|
227
|
+
}
|
|
228
|
+
function follow(dt) {
|
|
229
|
+
const ease = 1 - Math.exp(-dt / 90);
|
|
230
|
+
const slow = 1 - Math.exp(-dt / 300);
|
|
231
|
+
pointer.ringX += (pointer.x - pointer.ringX) * ease;
|
|
232
|
+
pointer.ringY += (pointer.y - pointer.ringY) * ease;
|
|
233
|
+
pointer.fieldX += (pointer.x - pointer.fieldX) * slow;
|
|
234
|
+
pointer.fieldY += (pointer.y - pointer.fieldY) * slow;
|
|
235
|
+
pointer.alpha += ((pointer.active ? 1 : 0) - pointer.alpha) * ease;
|
|
236
|
+
const scale = pointer.down ? .72 : pointer.hover ? 1.35 : 1;
|
|
237
|
+
follower.style.transform = `translate3d(${pointer.ringX - 15}px, ${pointer.ringY - 15}px, 0) scale(${scale})`;
|
|
238
|
+
follower.style.opacity = String(pointer.alpha * (pointer.hover ? .35 : .8));
|
|
239
|
+
const span = uniforms.uSpan.value;
|
|
240
|
+
uniforms.uPointer.value.set((pointer.fieldX / width - .5) * span.x, (.5 - pointer.fieldY / height) * span.y);
|
|
241
|
+
uniforms.uPointerAlpha.value = pointer.alpha;
|
|
242
|
+
const targetX = pointer.active ? (pointer.x / width - .5) * span.x : 0;
|
|
243
|
+
const targetY = pointer.active ? (.5 - pointer.y / height) * span.y : 0;
|
|
244
|
+
const parallax = uniforms.uParallax.value;
|
|
245
|
+
parallax.x += (targetX - parallax.x) * slow;
|
|
246
|
+
parallax.y += (targetY - parallax.y) * slow;
|
|
247
|
+
}
|
|
248
|
+
function addDust(now) {
|
|
249
|
+
if (!dustGeometry || now - lastTrail < 28 || Math.hypot(pointer.x - lastTrailX, pointer.y - lastTrailY) < 5) return;
|
|
250
|
+
const span = uniforms.uSpan.value;
|
|
251
|
+
const index = trailIndex++ % trailCount;
|
|
252
|
+
const position = dustGeometry.getAttribute('position');
|
|
253
|
+
position.setXYZ(index, (pointer.x / width - .5) * span.x, (.5 - pointer.y / height) * span.y, 0);
|
|
254
|
+
position.needsUpdate = true;
|
|
255
|
+
const births = dustGeometry.getAttribute('aBirth');
|
|
256
|
+
births.setX(index, elapsed);
|
|
257
|
+
births.needsUpdate = true;
|
|
258
|
+
lastTrail = now; lastTrailX = pointer.x; lastTrailY = pointer.y;
|
|
259
|
+
}
|
|
260
|
+
function tick(now) {
|
|
261
|
+
frame = 0;
|
|
262
|
+
if (document.hidden || isStatic() || lost) return;
|
|
263
|
+
if (lastFrame && now - lastFrame < 31) { schedule(); return; }
|
|
264
|
+
const dt = lastFrame ? Math.min(now - lastFrame, 64) : 33;
|
|
265
|
+
lastFrame = now;
|
|
266
|
+
elapsed += dt / 1000;
|
|
267
|
+
uniforms.uTime.value = elapsed;
|
|
268
|
+
follow(dt);
|
|
269
|
+
render();
|
|
270
|
+
schedule();
|
|
271
|
+
}
|
|
272
|
+
function resize() {
|
|
273
|
+
width = innerWidth; height = innerHeight;
|
|
274
|
+
resizeRenderer();
|
|
275
|
+
clearDust();
|
|
276
|
+
render();
|
|
277
|
+
}
|
|
278
|
+
window.addEventListener('pointermove', event => {
|
|
279
|
+
if (isStatic() || event.pointerType === 'touch') return;
|
|
280
|
+
pointer.x = event.clientX; pointer.y = event.clientY;
|
|
281
|
+
if (!pointer.active) {
|
|
282
|
+
pointer.ringX = pointer.fieldX = pointer.x;
|
|
283
|
+
pointer.ringY = pointer.fieldY = pointer.y;
|
|
284
|
+
lastTrailX = pointer.x; lastTrailY = pointer.y;
|
|
285
|
+
}
|
|
286
|
+
pointer.active = true;
|
|
287
|
+
pointer.hover = !!event.target.closest('a, button, input, textarea, select, summary');
|
|
288
|
+
addDust(performance.now());
|
|
289
|
+
schedule();
|
|
290
|
+
}, { passive: true });
|
|
291
|
+
window.addEventListener('pointerdown', event => {
|
|
292
|
+
if (isStatic() || event.pointerType === 'touch') return;
|
|
293
|
+
pointer.down = true; schedule();
|
|
294
|
+
}, { passive: true });
|
|
295
|
+
window.addEventListener('pointerup', () => { pointer.down = false; schedule(); }, { passive: true });
|
|
296
|
+
const leave = () => { pointer.active = false; pointer.down = false; schedule(); };
|
|
297
|
+
document.documentElement.addEventListener('pointerleave', leave);
|
|
298
|
+
window.addEventListener('blur', leave);
|
|
299
|
+
window.addEventListener('resize', resize, { passive: true });
|
|
300
|
+
document.addEventListener('visibilitychange', () => {
|
|
301
|
+
document.documentElement.dataset.effectsPaused = String(document.hidden);
|
|
302
|
+
if (document.hidden) stop(); else schedule();
|
|
303
|
+
});
|
|
304
|
+
canvas.addEventListener('webglcontextlost', event => {
|
|
305
|
+
event.preventDefault(); lost = true; stop();
|
|
306
|
+
document.documentElement.dataset.webgl = 'fallback';
|
|
307
|
+
});
|
|
308
|
+
canvas.addEventListener('webglcontextrestored', () => {
|
|
309
|
+
lost = false;
|
|
310
|
+
document.documentElement.dataset.webgl = 'ready';
|
|
311
|
+
clearDust(); render(); schedule();
|
|
312
|
+
});
|
|
313
|
+
new MutationObserver(updatePalette).observe(document.body, { attributes: true, attributeFilter: ['data-theme'] });
|
|
314
|
+
reducedMotion.addEventListener('change', updatePreferences);
|
|
315
|
+
finePointer.addEventListener('change', updatePreferences);
|
|
316
|
+
window.addEventListener('pagehide', () => stop());
|
|
317
|
+
window.addEventListener('pageshow', () => schedule());
|
|
318
|
+
// Previous comparison URLs now open this single star field.
|
|
319
|
+
const url = new URL(location.href);
|
|
320
|
+
if (url.searchParams.has('effect')) {
|
|
321
|
+
url.searchParams.delete('effect'); history.replaceState(null, '', url);
|
|
322
|
+
}
|
|
323
|
+
updatePreferences();
|
|
@@ -4,11 +4,19 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
6
|
<meta name="color-scheme" content="light dark">
|
|
7
|
-
<title>Codex Collab ·
|
|
7
|
+
<title>Codex Collab · 企业微信协作向导</title>
|
|
8
8
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
9
9
|
<link rel="stylesheet" href="/style.css">
|
|
10
|
+
<link rel="stylesheet" href="/effects.css">
|
|
10
11
|
</head>
|
|
11
|
-
<body>
|
|
12
|
+
<body data-theme="day">
|
|
13
|
+
<div id="ambient" aria-hidden="true">
|
|
14
|
+
<div class="ambient-light light-one"></div>
|
|
15
|
+
<div class="ambient-light light-two"></div>
|
|
16
|
+
<div class="ambient-light light-three"></div>
|
|
17
|
+
<canvas id="ambient-canvas"></canvas>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="cursor-follower" aria-hidden="true"><i></i></div>
|
|
12
20
|
<header class="masthead">
|
|
13
21
|
<a class="brand" href="/" data-view="home" aria-label="Codex Collab 首页">
|
|
14
22
|
<span class="brand-mark" aria-hidden="true">⌘</span>Codex Collab</a>
|
|
@@ -31,8 +39,8 @@
|
|
|
31
39
|
</span>
|
|
32
40
|
</div>
|
|
33
41
|
<div class="intro">
|
|
34
|
-
<h1 tabindex="-1"
|
|
35
|
-
<p
|
|
42
|
+
<h1 tabindex="-1">让 Codex 组队工作。</h1>
|
|
43
|
+
<p>在企业微信群里,连接你和同事的 Codex 会话,按角色分工、接力完成任务。</p>
|
|
36
44
|
</div>
|
|
37
45
|
<div id="profiles" class="project-list" aria-label="我的项目">
|
|
38
46
|
</div>
|
|
@@ -659,9 +667,6 @@
|
|
|
659
667
|
<button class="theme-choice" data-theme-choice="day" aria-pressed="true">
|
|
660
668
|
<i aria-hidden="true">
|
|
661
669
|
</i>纸白</button>
|
|
662
|
-
<button class="theme-choice" data-theme-choice="studio" aria-pressed="false">
|
|
663
|
-
<i aria-hidden="true">
|
|
664
|
-
</i>雾绿</button>
|
|
665
670
|
<button class="theme-choice" data-theme-choice="night" aria-pressed="false">
|
|
666
671
|
<i aria-hidden="true">
|
|
667
672
|
</i>夜色</button>
|
|
@@ -669,5 +674,6 @@
|
|
|
669
674
|
</footer>
|
|
670
675
|
<script src="/app.js" defer>
|
|
671
676
|
</script>
|
|
677
|
+
<script src="/effects.js" type="module"></script>
|
|
672
678
|
</body>
|
|
673
679
|
</html>
|
|
@@ -6,7 +6,6 @@ body {
|
|
|
6
6
|
--accent: #355d49; --on-accent: #fff; --wash: #eaf0e6; --hover: #f1f3eb; --shadow: #355d4909;
|
|
7
7
|
color-scheme: light; margin: 0; background: var(--bg); color: var(--ink); min-height: 100dvh; display: flex; flex-direction: column;
|
|
8
8
|
}
|
|
9
|
-
body[data-theme="studio"] { --bg: #f0f4ef; --surface: #f8fbf6; --ink: #2c4338; --muted: #809181; --line: #d8e2d6; --accent: #426c50; --wash: #e0eadd; --hover: #e9f0e5; }
|
|
10
9
|
body[data-theme="night"] { color-scheme: dark; --bg: #131b19; --surface: #1a2420; --ink: #e9eee3; --muted: #82958b; --line: #2e3d34; --accent: #c3d5b0; --on-accent: #23372a; --wash: #2b3b2f; --hover: #1d2b22; --shadow: #0000000b; }
|
|
11
10
|
button, input { font: inherit; }
|
|
12
11
|
a { color: inherit; text-decoration: none; }
|
|
@@ -18,8 +17,8 @@ h1, p { margin: 0; }
|
|
|
18
17
|
h1:focus { outline: none; }
|
|
19
18
|
.icon { width: 20px; height: 20px; flex-shrink: 0; }
|
|
20
19
|
.masthead { display: flex; align-items: center; justify-content: space-between; padding: 30px 44px; gap: 16px; }
|
|
21
|
-
.brand { display: inline-flex; align-items: center; gap:
|
|
22
|
-
.brand-mark { display: inline-grid; place-items: center; width:
|
|
20
|
+
.brand { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; letter-spacing: -.4px; font-weight: 550; }
|
|
21
|
+
.brand-mark { display: inline-grid; place-items: center; width: 24px; height: 27px; font: 24px/1 sans-serif; color: var(--accent); }
|
|
23
22
|
.preview-label { font-size: 10px; color: var(--muted); letter-spacing: 1px; }
|
|
24
23
|
main { width: 100%; flex: 1; padding: 50px 24px 66px; display: flex; justify-content: center; align-items: flex-start; }
|
|
25
24
|
.scene { width: 100%; max-width: 536px; }
|
|
@@ -70,7 +69,6 @@ body[data-theme="night"] .project-icon.sand { color: #cfbc9b; background: #3c372
|
|
|
70
69
|
.theme-picker button[aria-pressed="true"] { color: var(--ink); opacity: 1; }
|
|
71
70
|
.theme-picker button:hover { opacity: 1; }
|
|
72
71
|
.theme-picker i { width: 7px; height: 7px; border-radius: 50%; border: 1px solid #c4cbbd; background: #faf9f6; }
|
|
73
|
-
.theme-picker [data-theme-choice="studio"] i { background: #bac9b5; border-color: #a8bba4; }
|
|
74
72
|
.theme-picker [data-theme-choice="night"] i { background: #344a3e; border-color: #4c6353; }
|
|
75
73
|
.back-link { display: inline-flex; align-items: center; gap: 7px; font-size: 11px; color: var(--muted); padding: 6px 0; margin: 0 0 24px; }
|
|
76
74
|
.back-link:hover { color: var(--accent); }
|
|
@@ -156,8 +154,6 @@ html[data-page-hidden="true"] *, html[data-page-hidden="true"] *::before, html[d
|
|
|
156
154
|
@media (min-height: 950px) and (min-width: 700px) { main { padding-top: 80px; } }
|
|
157
155
|
@media (max-width: 600px) {
|
|
158
156
|
.masthead { padding: 24px; }
|
|
159
|
-
.brand { font-size: 13px; gap: 8px; }
|
|
160
|
-
.brand-mark { font-size: 24px; width: 24px; }
|
|
161
157
|
.preview-label { font-size: 9px; }
|
|
162
158
|
main { padding: 38px 24px 52px; }
|
|
163
159
|
h1 { font-size: 30px; letter-spacing: -1.2px; }
|
|
@@ -216,7 +212,6 @@ html[data-page-hidden="true"] *, html[data-page-hidden="true"] *::before, html[d
|
|
|
216
212
|
|
|
217
213
|
/* Live onboarding uses the same quiet layout as the design preview. */
|
|
218
214
|
body { --muted: #6e7d73; }
|
|
219
|
-
body[data-theme="studio"] { --muted: #687d6d; }
|
|
220
215
|
body[data-theme="night"] { --muted: #a1b1a7; }
|
|
221
216
|
main { flex-direction: column; justify-content: flex-start; align-items: center; }
|
|
222
217
|
main > section { width: 100%; }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 0.10.0:让 Codex 组队工作
|
|
2
|
+
|
|
3
|
+
将已确认的星尘设计接入正式本机向导。首页说明两种协作方式:在企业微信群里连接自己的多个 Codex 会话,或与同事的 Codex 会话按角色分工、接力完成任务。
|
|
4
|
+
|
|
5
|
+
- 首页标题更新为“让 Codex 组队工作。”,桌面与移动端统一使用紧凑 Logo。
|
|
6
|
+
- 配色保留纸白、夜色;旧的雾绿选择自动回落到纸白。
|
|
7
|
+
- 增加 Three.js 星尘背景、轻微鼠标视差、星点牵引与短尾迹,移除鼠标光晕、手动停用按钮和背景说明文案。
|
|
8
|
+
- Three.js 固定版本,模块由本机向导提供,无需 CDN 或额外配置。背景不拦截点击,保留系统鼠标。
|
|
9
|
+
- 触屏与系统减少动态效果设置下使用静态星点,不创建 WebGL 上下文。无 WebGL 或上下文丢失时自动降级;页面隐藏时暂停动画。星点与尾迹各一次绘制,动画约 30 帧,像素比最多 1.5,画布最长边最多 2048 像素。
|
|
10
|
+
|
|
11
|
+
## 升级
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install --global xuym-codex-collab@0.10.0
|
|
15
|
+
codex-collab setup
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
已有项目配置和协作记录无需迁移。升级后重启已运行的本机工作台进程,再打开向导,才能加载新代码;已有机器人连接按原有配置使用。
|
|
19
|
+
|
|
20
|
+
本次通过 216 项自动化测试,并在浏览器验证正式页面及临时目录安装后的 npm 包:项目归属、主题切换保留表单、320–3840 像素布局、后台暂停与恢复、触屏与减少动态效果、WebGL 降级与恢复均通过;安装后的 CLI 能启动独立工作台并提供完整背景资源。不新增真实企业微信群或跨机器验收结论。
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# 0.10.1
|
|
2
|
+
|
|
3
|
+
修复升级 npm 包后重新打开向导时,旧后台服务仍被复用,导致星尘资源无法加载、页面样式不完整的问题。旧服务持有旧的路由表,却读取安装目录里更新后的 HTML,新增资源会返回 401 或 404。
|
|
4
|
+
|
|
5
|
+
- 后台服务记录启动时的软件版本、入口和 Node 路径。setup 在复用服务前检查这些信息;旧版本和未记录版本的服务会经过本机令牌验证,再正常退出并由新版替换。
|
|
6
|
+
- 并发执行多个 setup 会串行处理升级,同版本继续复用同一个服务。无法确认旧服务身份或旧服务未及时退出时停止升级,不强制结束进程。
|
|
7
|
+
- 服务切换保留项目配置、Codex 对话绑定、机器人运行意图和独立 Runner;打开向导时使用切换后的链接。
|
|
8
|
+
|
|
9
|
+
升级后运行:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install --global xuym-codex-collab@0.10.1 --prefer-online
|
|
13
|
+
codex-collab setup
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
回归覆盖旧服务迁移、版本不一致时替换、并发打开、同版本复用,以及无法验证身份时保留原进程;检查升级后的全部星尘与 Three.js 资源可访问,业务 API 仍要求登录。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xuym-codex-collab",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "Git-backed collaboration tools for Codex teams, with MCP, WeCom, and local runners.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"@modelcontextprotocol/server": "2.3.1",
|
|
58
58
|
"@openai/codex-sdk": "0.161.0",
|
|
59
59
|
"@wecom/aibot-node-sdk": "1.0.7",
|
|
60
|
+
"three": "0.186.1",
|
|
60
61
|
"zod": "^4.6.5"
|
|
61
62
|
}
|
|
62
63
|
}
|