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.
- package/README.md +7 -3
- package/apps/onboarding/bundle.mjs +1 -6
- package/apps/onboarding/guided.mjs +2 -2
- package/apps/onboarding/manager.mjs +73 -48
- package/apps/onboarding/projects.mjs +99 -0
- package/apps/onboarding/server.mjs +2 -1
- package/apps/onboarding/web/app.js +141 -12
- package/apps/onboarding/web/index.html +8 -6
- package/apps/onboarding/web/style.css +14 -10
- package/apps/peer/desktop.mjs +4 -3
- package/apps/peer/mcp.mjs +1 -1
- package/apps/peer/runner.mjs +4 -3
- package/apps/peer/runtime.mjs +6 -1
- package/apps/peer/wecom.mjs +1 -1
- package/docs/desktop-poll.md +2 -2
- package/docs/onboarding-audit.md +12 -1
- package/docs/onboarding.md +30 -6
- package/docs/peer.md +4 -2
- package/docs/team-registry.md +7 -1
- package/docs/terminal-onboarding.md +3 -3
- package/package.json +1 -1
- package/packages/codex-kit/templates/.agents/skills/team-collab/SKILL.md +2 -2
- package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +2 -2
- package/packages/peer/config.mjs +2 -0
- package/packages/peer/git.mjs +14 -0
- package/packages/peer/protocol.mjs +4 -2
- package/packages/peer/repository.mjs +67 -0
- package/packages/peer/service.mjs +11 -8
- package/packages/peer/store.mjs +16 -16
- package/schemas/peer-task.schema.json +15 -13
- package/schemas/team-policy.schema.json +12 -1
- package/scripts/peer/cli.mjs +2 -5
- package/scripts/peer/init-agent.mjs +6 -15
- package/scripts/peer/onboarding.mjs +26 -20
- package/scripts/peer/refresh-project.mjs +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 安装与入口
|
|
6
6
|
|
|
7
|
-
需要 Node.js 20+、Git、Codex 桌面应用,以及业务仓库的读取和 push
|
|
7
|
+
需要 Node.js 20+、Git、Codex 桌面应用,以及业务仓库的读取和 push 权限。业务仓库需已有至少一个远程业务分支;可以使用独立的本机 clone,也可以让向导拉取新目录。成员无需 clone 本工具源码。
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install --global xuym-codex-collab
|
|
@@ -20,7 +20,11 @@ codex-collab
|
|
|
20
20
|
|
|
21
21
|
管理员先在企业微信创建**独立智能机器人**并加入协作群,再填写 Bot ID/Secret。工具通过群验证码发现群 `chatid` 和真人 `userid`,不需要成员手工寻找这些 ID、生成密钥或编辑 JSON。机器人 Secret 通过加密接入包交给指定成员;批量分配自动生成密码,可在“已分配的机器人”查看并单独复制。接入说明不含密码和 Secret。显示名称支持中文,职责可从前端、后端、全栈、测试等预设中选择;内部 ID 和自定义标签放在高级选项中。
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
协作远程与业务基准分支自动识别;不要求 `origin/main`。有多个不同远程或默认分支不明确时,管理员选择一次。成员与新增机器人继承签名团队基准,各自可使用不同远程别名。每个新任务固定创建时的 commit,排队和重试期间不会漂移;管理员可在团队设置里为后续任务修改基准分支。
|
|
24
|
+
|
|
25
|
+
工作台首页按项目汇总,同一仓库的多个本机机器人共用一张项目卡片。打开项目后,所有团队机器人展示在同一列表里,自己的标记“我的”;本机已接入的机器人可继续配置或管理,其他机器人的职责来自签名团队记录。界面使用轻量 CSS 动效,支持系统减少动态效果设置,页面进入后台时暂停动画。
|
|
26
|
+
|
|
27
|
+
同一个真人可以拥有多个 Agent,每个 Agent 使用独立机器人、签名身份、业务 clone 和 Codex 对话。管理员在项目页点击“添加我的机器人”,验证同一个企业微信账号后自动接入,无需下载包或填写包密码;给同事分配时仍使用加密接入包,同事可以领取多份。相同 userid 不会合并 Agent 权限,审核通知会指明应使用的机器人。
|
|
24
28
|
|
|
25
29
|
机器人在企业微信后台设为多人可用并加入协作群后,已批准入队的成员可以 `@` 任意团队机器人的自然语言需求;未入队的群成员仍会被工具拒绝。普通成员可提交需求、创建任务和查询状态,审核与取消、完成等控制操作继续受原有身份权限限制。接入完成后,团队成员可直接互相派发,不必再改机器人配置。
|
|
26
30
|
|
|
@@ -51,7 +55,7 @@ codex-collab autostart status
|
|
|
51
55
|
|
|
52
56
|
`doctor` 检查项目 MCP/Skill、机器人进程与认证、对话绑定、真实轮询以及 Git 读取和推送权限,并给出下一步。页面也提供“检查整条连接”和“修复本机配置”。修复更新已识别的生成配置,并恢复缺失的 MCP 配置和 Skill;自定义、符号链接或已提交的共享配置需要手动合并。修复后请在 Codex 重新加载项目 MCP,在原对话执行启用指令。
|
|
53
57
|
|
|
54
|
-
升级执行 `npm install -g xuym-codex-collab@latest`,然后重新打开工作台并修复项目配置。已经运行的 Node 进程需要重启才能加载新代码:停止并重新启动机器人;后台工作台可等下次登录,或终止 `codex-collab service-status` 显示的工作台 PID 后重新执行 `codex-collab`。0.5.0 迁移时只接管仍在运行的旧连接,已停止的旧连接需要你主动启动。
|
|
58
|
+
升级执行 `npm install -g xuym-codex-collab@latest`,然后重新打开工作台并修复项目配置。已经运行的 Node 进程需要重启才能加载新代码:停止并重新启动机器人;后台工作台可等下次登录,或终止 `codex-collab service-status` 显示的工作台 PID 后重新执行 `codex-collab`。0.6.0 增加任务 v3 规格:团队全员须先升级并重启工作台、Runner 与 MCP,再创建新任务;历史任务保持原规则。0.5.0 迁移时只接管仍在运行的旧连接,已停止的旧连接需要你主动启动。
|
|
55
59
|
|
|
56
60
|
`codex-collab autostart off` 关闭登录自启,`autostart on` 重新设置。`setup --no-startup` 仅跳过本次自启配置,`setup --foreground` 以前台模式运行,便于排查。Windows 支持独立后台工作台,暂未自动配置登录启动。电脑休眠、关机或退出系统期间不会执行群任务;Codex 对话的自动化仍需保持启用。
|
|
57
61
|
|
|
@@ -17,9 +17,4 @@ export function decryptBundle(value, password) {
|
|
|
17
17
|
return JSON.parse(Buffer.concat([decipher.update(Buffer.from(value.ciphertext, 'base64')), decipher.final()]).toString('utf8'));
|
|
18
18
|
} catch { throw new SetupError('接入包或密码无效,请确认文件完整且密码正确。'); }
|
|
19
19
|
}
|
|
20
|
-
export
|
|
21
|
-
const scp = remote.match(/^(?:[^@/]+@)?([^:/]+):(.+)$/);
|
|
22
|
-
try { const url = new URL(remote); if (['https:', 'http:', 'ssh:'].includes(url.protocol)) return `${url.hostname.toLowerCase()}/${url.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '')}`; } catch {}
|
|
23
|
-
if (scp && !remote.startsWith('/')) return `${scp[1].toLowerCase()}/${scp[2].replace(/\/+$/g, '').replace(/\.git$/, '')}`;
|
|
24
|
-
return remote;
|
|
25
|
-
}
|
|
20
|
+
export { remoteIdentity } from '../../packages/peer/repository.mjs';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
+
import { resolveRemote } from '../../packages/peer/repository.mjs';
|
|
1
2
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
3
|
import { lstat, mkdir, readFile, readdir, realpath, rename, rm, rmdir } from 'node:fs/promises';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
6
|
import { git } from '../../packages/peer/store.mjs';
|
|
6
|
-
import { remoteIdentity } from './bundle.mjs';
|
|
7
7
|
import { SetupError } from './errors.mjs';
|
|
8
8
|
|
|
9
9
|
export const responsibilities = {
|
|
@@ -80,7 +80,7 @@ export async function cloneRepository({ remote, destination, privateHome }) {
|
|
|
80
80
|
if (existing.isSymbolicLink()) throw new SetupError('拉取目录不能是符号链接,请选择独立目录。');
|
|
81
81
|
try {
|
|
82
82
|
const actual = await realpath(target);
|
|
83
|
-
if (await git(actual, 'rev-parse', '--show-toplevel') === actual &&
|
|
83
|
+
if (await git(actual, 'rev-parse', '--show-toplevel') === actual && !!await resolveRemote(actual, { expectedRemote: remote })) return actual;
|
|
84
84
|
} catch { /* Reject existing unrelated folders instead of overwriting them. */ }
|
|
85
85
|
throw new SetupError('目标目录已存在,请选择新的目录,或切换到使用已有项目。');
|
|
86
86
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { inspectRepository, RepositoryError, resolveRemote, teamBaseBranch } from '../../packages/peer/repository.mjs';
|
|
1
2
|
import { defaultReviewRules } from '../../packages/peer/review.mjs';
|
|
2
3
|
import { SetupError } from './errors.mjs';
|
|
4
|
+
import { groupProjects, projectId, projectView } from './projects.mjs';
|
|
3
5
|
import { createPublicKey, randomBytes, randomUUID } from 'node:crypto';
|
|
4
6
|
import { chmod, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from 'node:fs/promises';
|
|
5
7
|
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
@@ -53,9 +55,8 @@ function gitFailure(error) {
|
|
|
53
55
|
if (/could not resolve host|failed to connect|network is unreachable|connection refused|network failure/.test(detail)) {
|
|
54
56
|
return new SetupError('无法连接远程仓库,请检查网络或代理后重试。');
|
|
55
57
|
}
|
|
56
|
-
return new SetupError('Git
|
|
58
|
+
return new SetupError('Git 无法读取远程仓库,请检查所选远程地址、Git 权限和网络后重试。');
|
|
57
59
|
}
|
|
58
|
-
function shellQuote(value) { return `'${String(value).replaceAll("'", "'\\''")}'`; }
|
|
59
60
|
function mcpSection(text, serverName) {
|
|
60
61
|
let active = false; const lines = [];
|
|
61
62
|
for (const line of String(text || '').split(/\r?\n/)) {
|
|
@@ -74,7 +75,7 @@ function projectMcpConfigured(projectConfig, configPath, config) {
|
|
|
74
75
|
const section = mcpSection(projectConfig, `team-collab-${config.agent_id}`);
|
|
75
76
|
return !/^\s*enabled\s*=\s*false\s*(?:#.*)?$/m.test(section) && paths.some((path) => section.includes(JSON.stringify(path)));
|
|
76
77
|
}
|
|
77
|
-
async function ensureRepo(path) {
|
|
78
|
+
async function ensureRepo(path, options = {}) {
|
|
78
79
|
if (typeof path !== 'string' || !isAbsolute(path)) throw new SetupError('请选择业务仓库的本机绝对路径。');
|
|
79
80
|
let repo;
|
|
80
81
|
try { repo = await realpath(path); } catch { throw new SetupError('请选择存在的业务仓库目录。'); }
|
|
@@ -84,25 +85,10 @@ async function ensureRepo(path) {
|
|
|
84
85
|
if (error instanceof SetupError) throw error;
|
|
85
86
|
throw new SetupError('请选择 Git 仓库根目录。');
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
catch (error) {
|
|
90
|
-
const detail = `${error?.stderr || ''}\n${error?.message || ''}`.toLowerCase();
|
|
91
|
-
if (/no such remote|does not appear to be a git repository/.test(detail)) throw new SetupError('该目录没有名为 origin 的远程仓库,请先配置 GitHub origin。');
|
|
92
|
-
throw gitFailure(error);
|
|
93
|
-
}
|
|
94
|
-
let main;
|
|
95
|
-
try { main = await git(repo, 'ls-remote', '--heads', 'origin', 'refs/heads/main'); }
|
|
96
|
-
catch (error) { throw gitFailure(error); }
|
|
97
|
-
if (!main) {
|
|
98
|
-
let heads;
|
|
99
|
-
try { heads = await git(repo, 'ls-remote', '--heads', 'origin'); }
|
|
100
|
-
catch (error) { throw gitFailure(error); }
|
|
101
|
-
if (!heads) throw new SetupError(`远程仓库为空,未找到 origin/main。请先在终端执行:\ngit -C ${shellQuote(repo)} push -u origin main`);
|
|
102
|
-
throw new SetupError(`远程仓库可访问,但没有 main 分支。请先将本地 main 推送到 origin:\ngit -C ${shellQuote(repo)} push -u origin main`);
|
|
103
|
-
}
|
|
104
|
-
return { repo, remote };
|
|
88
|
+
try { return { repo, ...await (options.remoteOnly ? resolveRemote(repo, options) : inspectRepository(repo, options)) }; }
|
|
89
|
+
catch (error) { if (error instanceof RepositoryError) throw new SetupError(error.message); throw gitFailure(error); }
|
|
105
90
|
}
|
|
91
|
+
|
|
106
92
|
function validateBundle(value) {
|
|
107
93
|
if (!value || value.schema_version !== 1 || value.kind !== 'codex-collab-invite' || typeof value.remote !== 'string' || !value.remote || !value.history || !value.invitation || !value.wecom || typeof value.wecom.secret !== 'string' || !value.wecom.secret) throw new SetupError('接入包格式无效。');
|
|
108
94
|
const history = validateHistory(value.history, value.root_hash);
|
|
@@ -130,15 +116,21 @@ export class SetupManager {
|
|
|
130
116
|
async profileStore(state, { enrollmentOnly = false } = {}) {
|
|
131
117
|
const identity = await readPrivateJson(state.identity_path);
|
|
132
118
|
const initial = state.bootstrap_team;
|
|
133
|
-
return new PeerGitStore({ repo: state.repo, dataDir: state.data_dir || join(this.path(state.id), 'data'), team: initial,
|
|
119
|
+
return new PeerGitStore({ remoteName: state.remote_name, remoteUrl: state.remote, repo: state.repo, dataDir: state.data_dir || join(this.path(state.id), 'data'), team: initial,
|
|
134
120
|
agentId: state.agent_id, privateKey: identity.private_key, registryRoot: state.root_hash, enrollmentOnly }).init();
|
|
135
121
|
}
|
|
122
|
+
async localStates() {
|
|
123
|
+
await mkdir(join(this.home, 'profiles'), { recursive: true, mode: 0o700 });
|
|
124
|
+
const ids = (await readdir(join(this.home, 'profiles'))).filter((id) => uuidPattern.test(id)).sort();
|
|
125
|
+
return Promise.all(ids.map((id) => this.state(id)));
|
|
126
|
+
}
|
|
127
|
+
project(id, options) { return projectView(this, id, options); }
|
|
136
128
|
async overview() {
|
|
137
129
|
await mkdir(join(this.home, 'profiles'), { recursive: true, mode: 0o700 });
|
|
138
|
-
const
|
|
130
|
+
const states = await this.localStates();
|
|
139
131
|
const profiles = [], removed_profiles = [];
|
|
140
|
-
for (const
|
|
141
|
-
const
|
|
132
|
+
for (const state of states) {
|
|
133
|
+
const id = state.id;
|
|
142
134
|
(state.phase === 'removed' ? removed_profiles : profiles).push({ id, agent_id: state.agent_id, role: state.role, display_name: state.display_name || state.agent_id, repo: state.repo, phase: state.phase, error: state.last_error || null });
|
|
143
135
|
}
|
|
144
136
|
const drafts = [];
|
|
@@ -148,7 +140,7 @@ export class SetupManager {
|
|
|
148
140
|
if (!draft.profile_id) drafts.push({ id: draft.id, agent_id: draft.input.agent_id, display_name: draft.input.display_name, repo: draft.input.repo });
|
|
149
141
|
}
|
|
150
142
|
let context = {}; try { context = await readPrivateJson(join(this.home, 'context.json')); } catch {}
|
|
151
|
-
return { profiles, removed_profiles, drafts, defaults: await localDefaults(context.cwd || this.home), startup: await startupStatus(this.home) };
|
|
143
|
+
return { projects: groupProjects(states), profiles, removed_profiles, drafts, defaults: await localDefaults(context.cwd || this.home), startup: await startupStatus(this.home) };
|
|
152
144
|
}
|
|
153
145
|
async checkBotAvailable(botId) {
|
|
154
146
|
for (const profile of (await this.overview()).profiles) {
|
|
@@ -167,7 +159,7 @@ export class SetupManager {
|
|
|
167
159
|
}
|
|
168
160
|
await this.checkBotAvailable(credentials.bot_id);
|
|
169
161
|
if (input.repo) {
|
|
170
|
-
const { repo } = await ensureRepo(input.repo);
|
|
162
|
+
const { repo } = await ensureRepo(input.repo, { remoteName: input.remote_name, expectedRemote: input.remote_url, remoteOnly: true });
|
|
171
163
|
const toml = await readFile(join(repo, '.codex/config.toml'), 'utf8').catch(() => '');
|
|
172
164
|
for (const match of toml.matchAll(/"((?:[^"\\]|\\.)*\.json)"/g)) {
|
|
173
165
|
try {
|
|
@@ -184,7 +176,7 @@ export class SetupManager {
|
|
|
184
176
|
async prepareProject(input) {
|
|
185
177
|
const path = input.repo_mode === 'clone'
|
|
186
178
|
? await cloneRepository({ remote: input.remote, destination: input.destination || suggestedRepo(input.remote, memberFields({ ...input, display_name: input.display_name || input.agent_id || 'owner' }).agent_id), privateHome: this.home }) : input.repo;
|
|
187
|
-
const selected = await ensureRepo(path);
|
|
179
|
+
const selected = await ensureRepo(path, { remoteName: input.remote_name, baseBranch: input.base_branch, allowSelection: true });
|
|
188
180
|
if (inside(selected.repo, this.home)) throw new SetupError('本机配置目录必须在业务仓库之外。');
|
|
189
181
|
return { ...selected, tests: await recommendTests(selected.repo) };
|
|
190
182
|
}
|
|
@@ -196,7 +188,8 @@ export class SetupManager {
|
|
|
196
188
|
const argv = input.test_command ? commandArgv(input.test_command) : testArgv(input.test_argv);
|
|
197
189
|
const selected = await this.prepareProject(input);
|
|
198
190
|
for (const profile of (await this.overview()).profiles) if (profile.repo === selected.repo) throw new SetupError('这个 clone 已接入其他身份,请选择独立 clone。');
|
|
199
|
-
|
|
191
|
+
if (selected.selection_required) throw new SetupError('请先检查项目并选择协作远程和业务基准分支。');
|
|
192
|
+
const draft = { id: randomUUID(), input: { ...fields, repo: selected.repo, remote_name: selected.remote_name, remote_url: selected.remote, base_branch: selected.base_branch, test_argv: argv, team_name: input.team_name?.trim() || null,
|
|
200
193
|
bot_id: input.bot_id, secret: input.secret } };
|
|
201
194
|
await savePrivate(this.draftPath(draft.id), draft);
|
|
202
195
|
return this.adminDraft({ id: draft.id });
|
|
@@ -207,7 +200,7 @@ export class SetupManager {
|
|
|
207
200
|
let session;
|
|
208
201
|
if (draft.discovery_id && !reconnect) { try { session = this.discovery.view(draft.discovery_id); if (['failed', 'expired', 'disconnected'].includes(session.status)) session = null; } catch {} }
|
|
209
202
|
if (!session) {
|
|
210
|
-
session = await this.beginDiscovery({ bot_id: draft.input.bot_id, secret: draft.input.secret, repo: draft.input.repo });
|
|
203
|
+
session = await this.beginDiscovery({ bot_id: draft.input.bot_id, secret: draft.input.secret, repo: draft.input.repo, remote_name: draft.input.remote_name, remote_url: draft.input.remote_url });
|
|
211
204
|
draft.discovery_id = session.id; await savePrivate(this.draftPath(id), draft);
|
|
212
205
|
}
|
|
213
206
|
return { ...session, draft_id: id, agent_id: draft.input.agent_id, repo: draft.input.repo };
|
|
@@ -228,18 +221,18 @@ export class SetupManager {
|
|
|
228
221
|
async createAdmin(input, { onSaved } = {}) {
|
|
229
222
|
const fields = memberFields(input), agentId = fields.agent_id;
|
|
230
223
|
const caps = fields.capabilities; const argv = input.test_command ? commandArgv(input.test_command) : testArgv(input.test_argv || ['sh', 'test.sh']);
|
|
231
|
-
const { repo, remote } = await ensureRepo(input.repo);
|
|
224
|
+
const { repo, remote, remote_name, base_branch } = await ensureRepo(input.repo, { remoteName: input.remote_name, expectedRemote: input.remote_url, baseBranch: input.base_branch });
|
|
232
225
|
if (inside(repo, this.home)) throw new SetupError('本机配置目录必须在业务仓库之外。');
|
|
233
226
|
for (const profile of (await this.overview()).profiles) if (profile.repo === repo) throw new SetupError('这个 clone 已接入其他身份,请选择独立 clone。');
|
|
234
227
|
const proof = this.discovery.consume(input.discovery_id);
|
|
235
228
|
const id = randomUUID(); const directory = this.path(id);
|
|
236
229
|
const identity = await getOrCreateIdentityFiles(agentId, join(this.home, 'identities'));
|
|
237
|
-
const team = buildTeamPolicy({ repo: input.repo_id?.trim() || remote, reviewer: agentId, defaultRoute: caps[0],
|
|
230
|
+
const team = buildTeamPolicy({ baseBranch: base_branch, repo: input.repo_id?.trim() || remote, reviewer: agentId, defaultRoute: caps[0],
|
|
238
231
|
agents: [{ id: agentId, role: 'admin', capabilities: caps, publicKey: await readFile(identity.public_key_file, 'utf8'), wecomUserid: proof.userid }],
|
|
239
232
|
routes: Object.fromEntries(caps.map((cap) => [cap, [agentId]])) });
|
|
240
233
|
team.schema_version = 3; team.review_rules = defaultReviewRules(team.routes);
|
|
241
234
|
const record = policyVersion(team, null, agentId, (await readPrivateJson(identity.private_key_file)).private_key);
|
|
242
|
-
const state = { id, role: 'admin', agent_id: agentId, display_name: fields.display_name, team_name: input.team_name || null, repo, remote, identity_path: identity.private_key_file, bootstrap_team: team,
|
|
235
|
+
const state = { id, role: 'admin', agent_id: agentId, display_name: fields.display_name, team_name: input.team_name || null, repo, remote, remote_name, identity_path: identity.private_key_file, bootstrap_team: team,
|
|
243
236
|
root_hash: recordHash(record), genesis: record, test_argv: argv,
|
|
244
237
|
wecom: { bot_id: proof.bot_id, secret: proof.secret, chat_ids: [proof.chatid], commands_enabled: true, allow_admin_commands: true }, phase: 'initializing' };
|
|
245
238
|
await this.save(state);
|
|
@@ -261,7 +254,7 @@ export class SetupManager {
|
|
|
261
254
|
const id = randomUUID(); const identityPath = join(this.path(id), 'identity.json');
|
|
262
255
|
await savePrivate(identityPath, identity);
|
|
263
256
|
let genesis, rootHash = config.team_registry_root;
|
|
264
|
-
const store = await new PeerGitStore({ repo: config.repo, dataDir: config.data_dir, team, agentId: config.agent_id, privateKey: config.private_key, registryRoot: rootHash }).init();
|
|
257
|
+
const store = await new PeerGitStore({ remoteName: config.remote_name, remoteUrl: config.remote_url, repo: config.repo, dataDir: config.data_dir, team, agentId: config.agent_id, privateKey: config.private_key, registryRoot: rootHash }).init();
|
|
265
258
|
if (!rootHash) {
|
|
266
259
|
const firstPath = join(store.checkout, '.agent-collab/team/versions/000001.json');
|
|
267
260
|
if (await exists(firstPath)) {
|
|
@@ -282,7 +275,7 @@ export class SetupManager {
|
|
|
282
275
|
}
|
|
283
276
|
}
|
|
284
277
|
const state = { id, role: team.agents[config.agent_id].role, agent_id: config.agent_id, repo: config.repo,
|
|
285
|
-
remote:
|
|
278
|
+
remote: store.remoteUrl, remote_name: store.remoteName, identity_path: identityPath, bootstrap_team: team, root_hash: rootHash,
|
|
286
279
|
test_argv: config.test_argv || ['sh', 'test.sh'], wecom: config.wecom, phase: 'ready', existing_config: resolve(config_path), data_dir: config.data_dir, attached: true };
|
|
287
280
|
await savePrivate(join(this.path(id), 'peer.json'), { ...config, team_registry_root: rootHash });
|
|
288
281
|
// Existing MCP processes keep using this path and the same private desktop state.
|
|
@@ -306,7 +299,7 @@ export class SetupManager {
|
|
|
306
299
|
if (state.phase === 'removed') throw new SetupError('该项目已移除,请从首页恢复接入。');
|
|
307
300
|
try {
|
|
308
301
|
if (state.role === 'admin' && state.phase === 'initializing') {
|
|
309
|
-
const bootstrap = new PeerGitStore({ repo: state.repo, dataDir: join(this.path(id), 'data'), team: state.bootstrap_team, agentId: state.agent_id,
|
|
302
|
+
const bootstrap = new PeerGitStore({ remoteName: state.remote_name, remoteUrl: state.remote, repo: state.repo, dataDir: join(this.path(id), 'data'), team: state.bootstrap_team, agentId: state.agent_id,
|
|
310
303
|
privateKey: (await readPrivateJson(state.identity_path)).private_key });
|
|
311
304
|
await bootstrap.init();
|
|
312
305
|
await bootstrap.registryTransaction(async () => {
|
|
@@ -358,7 +351,7 @@ export class SetupManager {
|
|
|
358
351
|
const teamPath = join(this.path(id), 'team.json'); await savePrivate(teamPath, store.team);
|
|
359
352
|
const configPath = join(this.path(id), 'peer.json');
|
|
360
353
|
if (!await exists(configPath)) await createMemberConfig({ teamPath, identityPath: state.identity_path, repoPath: state.repo,
|
|
361
|
-
dataDir: join(this.path(id), 'data'), configPath, testArgv: state.test_argv });
|
|
354
|
+
dataDir: join(this.path(id), 'data'), configPath, testArgv: state.test_argv, remoteName: state.remote_name, remoteUrl: state.remote });
|
|
362
355
|
const config = await readPrivateJson(configPath);
|
|
363
356
|
await savePrivate(configPath, { ...config, team_registry_root: state.root_hash, wecom: state.wecom });
|
|
364
357
|
await diagnosePeer(configPath, { testPush: true });
|
|
@@ -397,7 +390,7 @@ export class SetupManager {
|
|
|
397
390
|
async inspectBundle(input) {
|
|
398
391
|
const bundle = this.readBundle(input), invite = bundle.invitation;
|
|
399
392
|
return { agent_id: invite.agent_id, display_name: bundle.display_name || invite.agent_id, team_name: bundle.team_name || null,
|
|
400
|
-
remote: bundle.remote, capabilities: invite.capabilities, bot_id: invite.bot_id, suggested_repo: suggestedRepo(bundle.remote, invite.agent_id) };
|
|
393
|
+
remote: bundle.remote, base_branch: teamBaseBranch(validateHistory(bundle.history, bundle.root_hash).latest.team), capabilities: invite.capabilities, bot_id: invite.bot_id, suggested_repo: suggestedRepo(bundle.remote, invite.agent_id) };
|
|
401
394
|
}
|
|
402
395
|
async importBundle(input) {
|
|
403
396
|
const bundle = this.readBundle(input);
|
|
@@ -412,9 +405,9 @@ export class SetupManager {
|
|
|
412
405
|
}
|
|
413
406
|
async importAssignment(value, repo, ownAdminId) {
|
|
414
407
|
const { bundle, history } = validateBundle(value);
|
|
415
|
-
const selected = await ensureRepo(repo);
|
|
408
|
+
const selected = await ensureRepo(repo, { expectedRemote: bundle.remote, remoteOnly: true });
|
|
416
409
|
if (inside(selected.repo, this.home)) throw new SetupError('本机配置目录必须在业务仓库之外。');
|
|
417
|
-
if (remoteIdentity(selected.remote) !== remoteIdentity(bundle.remote)) throw new SetupError('
|
|
410
|
+
if (remoteIdentity(selected.remote) !== remoteIdentity(bundle.remote)) throw new SetupError('该目录的远程与邀请仓库不一致,请选择正确仓库;工具不会改写远程配置。');
|
|
418
411
|
let recovered;
|
|
419
412
|
for (const profile of (await this.overview()).profiles) {
|
|
420
413
|
const current = await this.state(profile.id);
|
|
@@ -433,7 +426,7 @@ export class SetupManager {
|
|
|
433
426
|
recovered = current;
|
|
434
427
|
}
|
|
435
428
|
const id = recovered?.id || randomUUID(); const identity = await getOrCreateIdentityFiles(bundle.invitation.agent_id, join(this.home, 'identities'));
|
|
436
|
-
const state = { id, role: 'member', agent_id: bundle.invitation.agent_id, display_name: bundle.display_name || bundle.invitation.agent_id, team_name: bundle.team_name || null, repo: selected.repo, remote: selected.remote, identity_path: identity.private_key_file,
|
|
429
|
+
const state = { id, role: 'member', agent_id: bundle.invitation.agent_id, display_name: bundle.display_name || bundle.invitation.agent_id, team_name: bundle.team_name || null, repo: selected.repo, remote: selected.remote, remote_name: selected.remote_name, identity_path: identity.private_key_file,
|
|
437
430
|
bootstrap_team: history.latest.team, root_hash: bundle.root_hash, bundle, test_argv: bundle.test_argv, phase: 'imported',
|
|
438
431
|
...(ownAdminId ? { own_admin_id: ownAdminId } : {}) };
|
|
439
432
|
await this.save(state); return this.detail(id, { localOnly: true });
|
|
@@ -631,7 +624,8 @@ export class SetupManager {
|
|
|
631
624
|
if (state.role !== 'admin' || state.phase !== 'ready') throw new SetupError('请从已接入的管理员项目添加自己的机器人。');
|
|
632
625
|
if (!idPattern.test(input.agent_id?.trim() || '')) throw new SetupError('请填写新 Agent 名称。');
|
|
633
626
|
const repo = input.repo_mode === 'clone' ? await cloneRepository({ remote: state.remote, destination: input.destination || suggestedRepo(state.remote, input.agent_id.trim()), privateHome: this.home }) : input.repo;
|
|
634
|
-
const
|
|
627
|
+
const team = (await this.profileStore(state)).team;
|
|
628
|
+
const selected = await ensureRepo(repo, { expectedRemote: state.remote, baseBranch: teamBaseBranch(team) });
|
|
635
629
|
if (inside(selected.repo, this.home)) throw new SetupError('本机配置目录必须在业务仓库之外。');
|
|
636
630
|
if (remoteIdentity(selected.remote) !== remoteIdentity(state.remote)) throw new SetupError('新 Agent 必须选择同一业务仓库的另一个 clone。');
|
|
637
631
|
const files = await readdir(join(this.path(input.id), 'exports')).catch((error) => { if (error.code === 'ENOENT') return []; throw error; });
|
|
@@ -674,6 +668,32 @@ export class SetupManager {
|
|
|
674
668
|
await savePrivate(config.team_file, store.team);
|
|
675
669
|
return this.detail(id);
|
|
676
670
|
}
|
|
671
|
+
async repositorySettings({ id }) {
|
|
672
|
+
const state = await this.state(id);
|
|
673
|
+
if (state.role !== 'admin' || state.phase !== 'ready') throw new SetupError('只有已接入的管理员可设置基准分支。');
|
|
674
|
+
const store = await this.profileStore(state);
|
|
675
|
+
const selected = await ensureRepo(state.repo, { remoteName: store.remoteName, expectedRemote: state.remote, allowSelection: true });
|
|
676
|
+
return { branches: selected.branches, base_branch: teamBaseBranch(store.team), default_branch: selected.base_branch, version: store.registry.latest.version };
|
|
677
|
+
}
|
|
678
|
+
async setRepositoryPolicy({ id, expected_version, base_branch }) {
|
|
679
|
+
const state = await this.state(id);
|
|
680
|
+
if (state.role !== 'admin' || state.phase !== 'ready') throw new SetupError('只有已接入的管理员可设置基准分支。');
|
|
681
|
+
if (!Number.isSafeInteger(expected_version) || expected_version < 1 || typeof base_branch !== 'string' || !base_branch) throw new SetupError('请重新读取团队基准分支后再保存。');
|
|
682
|
+
const store = await this.profileStore(state); const identity = await readPrivateJson(state.identity_path);
|
|
683
|
+
await ensureRepo(state.repo, { remoteName: store.remoteName, expectedRemote: state.remote, baseBranch: base_branch });
|
|
684
|
+
await store.registryTransaction(async (history) => {
|
|
685
|
+
if (history.latest.team.agents[state.agent_id]?.role !== 'admin') throw new SetupError('需要管理员权限。');
|
|
686
|
+
if (history.latest.version !== expected_version) throw new SetupError('团队规则已更新,请重新读取基准分支后再保存。');
|
|
687
|
+
const team = { ...structuredClone(history.latest.team), base_branch };
|
|
688
|
+
validateTeam(team);
|
|
689
|
+
if (canonical(team) === canonical(history.latest.team)) return { value: history.latest.version };
|
|
690
|
+
const version = policyVersion(team, history.latest, state.agent_id, identity.private_key);
|
|
691
|
+
return { files: [[versionFile(version), version]], value: version.version };
|
|
692
|
+
});
|
|
693
|
+
const { config } = await readPeerConfig(state.existing_config || join(this.path(id), 'peer.json'));
|
|
694
|
+
await savePrivate(config.team_file, store.team);
|
|
695
|
+
return this.detail(id);
|
|
696
|
+
}
|
|
677
697
|
async setReviewRules({ id, expected_version, review_rules }) {
|
|
678
698
|
const state = await this.state(id);
|
|
679
699
|
if (state.role !== 'admin' || state.phase !== 'ready') throw new SetupError('只有已接入的管理员可设置审核规则。');
|
|
@@ -694,11 +714,15 @@ export class SetupManager {
|
|
|
694
714
|
return this.detail(id);
|
|
695
715
|
}
|
|
696
716
|
async detail(id, { localOnly = false } = {}) {
|
|
697
|
-
const state = await this.state(id); const result = { id, agent_id: state.agent_id, display_name: state.display_name || state.agent_id, team_name: state.team_name || null, role: state.role, repo: state.repo, phase: state.phase, own_agent: !!state.own_admin_id, error: state.last_error || null };
|
|
717
|
+
const state = await this.state(id); const result = { id, project_id: projectId(state), agent_id: state.agent_id, display_name: state.display_name || state.agent_id, team_name: state.team_name || null, role: state.role, repo: state.repo, phase: state.phase, own_agent: !!state.own_admin_id, error: state.last_error || null };
|
|
698
718
|
if (state.phase === 'imported') result.assignment = { capabilities: state.bundle.invitation.capabilities, bot_id: state.bundle.invitation.bot_id,
|
|
699
719
|
expires_at: state.bundle.invitation.expires_at };
|
|
700
|
-
|
|
701
|
-
|
|
720
|
+
let synchronized;
|
|
721
|
+
if (state.phase === 'ready' && !localOnly) {
|
|
722
|
+
synchronized = await this.profileStore(state); result.base_branch = teamBaseBranch(synchronized.team); result.remote_name = synchronized.remoteName;
|
|
723
|
+
}
|
|
724
|
+
if (state.role === 'admin' && synchronized) {
|
|
725
|
+
const store = synchronized; const owners = new Map();
|
|
702
726
|
result.members = Object.entries(store.team.agents).map(([id, agent]) => {
|
|
703
727
|
if (agent.wecom_userid) {
|
|
704
728
|
const group = owners.get(agent.wecom_userid) || []; group.push(id); owners.set(agent.wecom_userid, group);
|
|
@@ -706,7 +730,7 @@ export class SetupManager {
|
|
|
706
730
|
return { id, capabilities: agent.capabilities, role: agent.role, verified_owner: !!agent.wecom_userid };
|
|
707
731
|
});
|
|
708
732
|
result.owners = [...owners].map(([userid, agent_ids]) => ({ agent_ids, is_you: userid === store.team.agents[state.agent_id]?.wecom_userid }));
|
|
709
|
-
result.routes = store.team.routes; result.review_rules = store.team.review_rules || null; result.policy_schema_version = store.team.schema_version; result.version = store.registry.latest.version; result.requests = [];
|
|
733
|
+
result.base_branch = teamBaseBranch(store.team); result.remote_name = store.remoteName; result.routes = store.team.routes; result.review_rules = store.team.review_rules || null; result.policy_schema_version = store.team.schema_version; result.version = store.registry.latest.version; result.requests = [];
|
|
710
734
|
const files = await readdir(join(store.checkout, registryPath, 'requests')).catch((error) => { if (error.code === 'ENOENT') return []; throw error; });
|
|
711
735
|
for (const file of files) {
|
|
712
736
|
try {
|
|
@@ -729,7 +753,8 @@ export class SetupManager {
|
|
|
729
753
|
}
|
|
730
754
|
if (state.phase === 'ready') {
|
|
731
755
|
result.config_path = state.existing_config || join(this.path(id), 'peer.json'); result.binding_prompt = bindingPrompt(state.agent_id, state.repo); result.detailed_binding_prompt = detailedBindingPrompt(state.agent_id, state.repo);
|
|
732
|
-
const config =
|
|
756
|
+
const { config, team: cachedTeam } = await readPeerConfig(result.config_path);
|
|
757
|
+
result.base_branch ??= teamBaseBranch(cachedTeam); result.remote_name ??= config.remote_name || state.remote_name;
|
|
733
758
|
const skillName = config.execution_mode === 'desktop_poll' ? 'team-collab-desktop' : 'team-collab';
|
|
734
759
|
const projectConfig = await readFile(join(state.repo, '.codex', 'config.toml'), 'utf8').catch((error) => error.code === 'ENOENT' ? '' : Promise.reject(error));
|
|
735
760
|
const installedSkill = await readFile(join(state.repo, '.agents', 'skills', skillName, 'SKILL.md'), 'utf8').catch((error) => error.code === 'ENOENT' ? '' : Promise.reject(error));
|
|
@@ -779,7 +804,7 @@ export class SetupManager {
|
|
|
779
804
|
add('binding', '目标 Codex 对话', !!value.desktop.binding, '复制启用指令,发送到你选定的 Codex 对话。');
|
|
780
805
|
add('poll', '对话实际轮询', value.poll_recent, '在绑定对话中执行启用指令,并确认该对话的协作自动化已启用。');
|
|
781
806
|
try { await diagnosePeer(value.config_path, { testPush: true }); add('git', 'Git 读取与推送权限', true); }
|
|
782
|
-
catch { add('git', 'Git 读取与推送权限', false, '请完成本机 Git
|
|
807
|
+
catch { add('git', 'Git 读取与推送权限', false, '请完成本机 Git 登录,确认所选远程和团队基准分支可读,并具有协作分支推送权限。'); }
|
|
783
808
|
}
|
|
784
809
|
const startup = await startupStatus(this.home);
|
|
785
810
|
return { id, checks, ready: value.can_receive_tasks === true, startup, heartbeat_confirmed: false,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createPublicKey } from 'node:crypto';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
import { digest, readPrivateJson } from '../../packages/peer/config.mjs';
|
|
4
|
+
import { remoteIdentity, teamBaseBranch } from '../../packages/peer/repository.mjs';
|
|
5
|
+
import { readRegistry, validateHistory } from '../../packages/peer/registry.mjs';
|
|
6
|
+
import { SetupError } from './errors.mjs';
|
|
7
|
+
|
|
8
|
+
export function projectId(state) {
|
|
9
|
+
return digest(state.remote && state.root_hash
|
|
10
|
+
? { repository: remoteIdentity(state.remote), root: state.root_hash }
|
|
11
|
+
: { profile: state.id });
|
|
12
|
+
}
|
|
13
|
+
export function groupProjects(states) {
|
|
14
|
+
const groups = new Map();
|
|
15
|
+
for (const state of states) {
|
|
16
|
+
if (state.phase === 'removed') continue;
|
|
17
|
+
const id = projectId(state);
|
|
18
|
+
if (!groups.has(id)) {
|
|
19
|
+
const repository = state.remote ? remoteIdentity(state.remote) : '';
|
|
20
|
+
groups.set(id, { id, name: basename(repository || state.repo || '').replace(/\.git$/, '') || '协作项目', repository, profile_ids: [], local_agent_count: 0 });
|
|
21
|
+
}
|
|
22
|
+
groups.get(id).profile_ids.push(state.id);
|
|
23
|
+
}
|
|
24
|
+
for (const group of groups.values()) {
|
|
25
|
+
group.profile_ids.sort();
|
|
26
|
+
group.local_agent_count = new Set(states.filter((state) => group.profile_ids.includes(state.id)).map((state) => state.agent_id)).size;
|
|
27
|
+
}
|
|
28
|
+
return [...groups.values()].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Project navigation aggregates local connections. It never creates a new team or
|
|
32
|
+
// changes the authorization of the existing per-agent operations.
|
|
33
|
+
export async function projectView(manager, id, { localOnly = false } = {}) {
|
|
34
|
+
const states = await manager.localStates();
|
|
35
|
+
const project = groupProjects(states).find((value) => value.id === id);
|
|
36
|
+
if (!project) throw new SetupError('未找到这个项目,请从“我的项目”重新打开。');
|
|
37
|
+
const local = states.filter((state) => project.profile_ids.includes(state.id));
|
|
38
|
+
const candidates = local.filter((state) => state.phase === 'ready').sort((a, b) => (a.role === 'admin' ? 0 : 1) - (b.role === 'admin' ? 0 : 1));
|
|
39
|
+
let team, synced = false;
|
|
40
|
+
if (!localOnly) {
|
|
41
|
+
for (const state of candidates) {
|
|
42
|
+
try { team = (await manager.profileStore(state)).team; synced = true; break; }
|
|
43
|
+
catch { /* Try another local connection without exposing Git/SDK errors. */ }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!team) {
|
|
47
|
+
let latest;
|
|
48
|
+
const use = (history) => { if (history && (!latest || history.latest.version > latest.version)) latest = history.latest; };
|
|
49
|
+
for (const state of [...candidates, ...local.filter((value) => value.phase !== 'ready')]) {
|
|
50
|
+
try {
|
|
51
|
+
const registry = await readRegistry(join(state.data_dir || join(manager.path(state.id), 'data'), 'peer-ledger'), state.root_hash);
|
|
52
|
+
use(registry);
|
|
53
|
+
} catch { /* The next source must also pass signature verification. */ }
|
|
54
|
+
try {
|
|
55
|
+
const history = state.bundle?.history || (state.genesis ? [state.genesis] : null);
|
|
56
|
+
if (history) use(validateHistory(history, state.root_hash));
|
|
57
|
+
} catch { /* Unverified membership never reaches the project roster. */ }
|
|
58
|
+
}
|
|
59
|
+
team = latest?.team;
|
|
60
|
+
}
|
|
61
|
+
const identities = new Map();
|
|
62
|
+
for (const state of local) {
|
|
63
|
+
try {
|
|
64
|
+
const identity = await readPrivateJson(state.identity_path);
|
|
65
|
+
identities.set(state.id, createPublicKey(identity.private_key).export({ type: 'spki', format: 'pem' }));
|
|
66
|
+
} catch { /* Missing local identity is visible as a connection problem. */ }
|
|
67
|
+
}
|
|
68
|
+
const matching = (state, agent) => identities.get(state.id) && identities.get(state.id) === agent.public_key;
|
|
69
|
+
const owners = new Set(local.flatMap((state) => {
|
|
70
|
+
const agent = team?.agents[state.agent_id];
|
|
71
|
+
return agent && matching(state, agent) && agent.wecom_userid ? [agent.wecom_userid] : [];
|
|
72
|
+
}));
|
|
73
|
+
const connections = new Map();
|
|
74
|
+
for (const state of local) {
|
|
75
|
+
let detail;
|
|
76
|
+
try { detail = await manager.detail(state.id, { localOnly: true }); } catch { /* Keep broken local connections recoverable. */ }
|
|
77
|
+
connections.set(state.id, { id: state.id, repo: state.repo, phase: state.phase, display_name: state.display_name || state.agent_id,
|
|
78
|
+
has_error: !!state.last_error || state.phase === 'ready' && !detail,
|
|
79
|
+
can_receive_tasks: detail?.can_receive_tasks === true, runner_online: detail?.runner?.online === true,
|
|
80
|
+
bound: !!detail?.desktop?.binding, poll_recent: detail?.poll_recent === true });
|
|
81
|
+
}
|
|
82
|
+
const included = new Set();
|
|
83
|
+
const agents = Object.entries(team?.agents || {}).map(([agentId, agent]) => {
|
|
84
|
+
const own = local.filter((state) => state.agent_id === agentId && matching(state, agent));
|
|
85
|
+
for (const state of own) included.add(state.id);
|
|
86
|
+
return { agent_id: agentId, role: agent.role, capabilities: agent.capabilities, mine: own.length > 0 || !!agent.wecom_userid && owners.has(agent.wecom_userid),
|
|
87
|
+
registered: true, connections: own.map((state) => connections.get(state.id)) };
|
|
88
|
+
});
|
|
89
|
+
for (const state of local) {
|
|
90
|
+
if (included.has(state.id)) continue;
|
|
91
|
+
agents.push({ agent_id: state.agent_id, role: state.role, capabilities: [], mine: true, registered: false,
|
|
92
|
+
identity_conflict: !!team?.agents[state.agent_id], connections: [connections.get(state.id)] });
|
|
93
|
+
}
|
|
94
|
+
agents.sort((a, b) => a.agent_id.localeCompare(b.agent_id));
|
|
95
|
+
const admin = candidates.find((state) => team?.agents[state.agent_id]?.role === 'admin' && matching(state, team.agents[state.agent_id]));
|
|
96
|
+
return { ...project, agents, admin_profile_id: admin?.id || null, base_branch: team ? teamBaseBranch(team) : null,
|
|
97
|
+
sync_status: synced ? 'synced' : team ? 'cached' : 'pending',
|
|
98
|
+
sync_error: !localOnly && !synced && candidates.length > 0 ? '团队同步暂未成功,正在显示本机已验证的记录。请检查 Git 连接后刷新。' : null };
|
|
99
|
+
}
|
|
@@ -36,12 +36,13 @@ export async function startSetupServer({ manager = new SetupManager(), port = 0
|
|
|
36
36
|
if (req.method === 'GET' && url.pathname === '/api/overview') return json(200, await manager.overview());
|
|
37
37
|
if (req.method === 'GET' && url.pathname === '/api/admin-draft') return json(200, await manager.exclusive(() => manager.adminDraft({ id: url.searchParams.get('id') })));
|
|
38
38
|
if (req.method === 'GET' && url.pathname === '/api/discovery') return json(200, manager.discovery.view(url.searchParams.get('id')));
|
|
39
|
+
if (req.method === 'GET' && url.pathname === '/api/project') return json(200, await manager.exclusive(() => manager.project(url.searchParams.get('id'), { localOnly: url.searchParams.get('local') === '1' })));
|
|
39
40
|
if (req.method === 'GET' && url.pathname === '/api/profile') return json(200, await manager.exclusive(() => manager.detail(url.searchParams.get('id'), { localOnly: url.searchParams.get('local') === '1' })));
|
|
40
41
|
if (req.method === 'GET' && url.pathname === '/api/export') {
|
|
41
42
|
const value = await manager.exclusive(() => manager.exportedBundle(url.searchParams.get('id'), url.searchParams.get('invite')));
|
|
42
43
|
res.writeHead(200, { 'Content-Type': 'application/json', 'Content-Disposition': `attachment; filename="${value.filename}"` }); res.end(value.content); return;
|
|
43
44
|
}
|
|
44
|
-
const actions = { diagnostics: () => manager.diagnostics(body), repair: () => manager.repair(body), 'project-check': () => manager.prepareProject(body), 'admin-prepare': () => manager.prepareAdmin(body), 'admin-complete': () => manager.completeAdmin(body), 'admin-retry': () => manager.adminDraft({ id: body.id, reconnect: true }), 'inspect-invite': () => manager.inspectBundle(body), progress: () => manager.progress(body), 'issue-batch': () => manager.issueBatch(body), 'reissue-invite': () => manager.reissueBundle(body), delivery: () => manager.delivery(body), 'reset-cancel': () => manager.resetCancel(body), 'reset-preview': () => manager.resetPreview(body), 'reset-discovery': () => manager.resetDiscovery(body), reset: () => manager.reset(body), 'own-agent': () => manager.createOwnAgent(body), 'own-agent-resume': () => manager.resumeOwnAgent(body), discovery: () => manager.beginDiscovery(body), admin: () => manager.createAdmin(body), import: () => manager.importBundle(body), existing: () => manager.importExisting(body), verify: () => manager.verifyMember(body), resume: () => manager.resume(body.id), issue: () => manager.issueBundle(body), approve: () => manager.approve(body), runner: () => manager.runner(body), 'review-rules': () => manager.setReviewRules(body) };
|
|
45
|
+
const actions = { 'repository-settings': () => manager.repositorySettings(body), 'repository-policy': () => manager.setRepositoryPolicy(body), diagnostics: () => manager.diagnostics(body), repair: () => manager.repair(body), 'project-check': () => manager.prepareProject(body), 'admin-prepare': () => manager.prepareAdmin(body), 'admin-complete': () => manager.completeAdmin(body), 'admin-retry': () => manager.adminDraft({ id: body.id, reconnect: true }), 'inspect-invite': () => manager.inspectBundle(body), progress: () => manager.progress(body), 'issue-batch': () => manager.issueBatch(body), 'reissue-invite': () => manager.reissueBundle(body), delivery: () => manager.delivery(body), 'reset-cancel': () => manager.resetCancel(body), 'reset-preview': () => manager.resetPreview(body), 'reset-discovery': () => manager.resetDiscovery(body), reset: () => manager.reset(body), 'own-agent': () => manager.createOwnAgent(body), 'own-agent-resume': () => manager.resumeOwnAgent(body), discovery: () => manager.beginDiscovery(body), admin: () => manager.createAdmin(body), import: () => manager.importBundle(body), existing: () => manager.importExisting(body), verify: () => manager.verifyMember(body), resume: () => manager.resume(body.id), issue: () => manager.issueBundle(body), approve: () => manager.approve(body), runner: () => manager.runner(body), 'review-rules': () => manager.setReviewRules(body) };
|
|
45
46
|
const key = url.pathname.replace('/api/', ''); const action = Object.hasOwn(actions, key) ? actions[key] : null;
|
|
46
47
|
if (req.method === 'POST' && url.pathname.startsWith('/api/') && action) return json(200, await manager.exclusive(action));
|
|
47
48
|
return json(404, { error: '操作不存在。' });
|