ai_co_discuss 0.2.95 → 0.2.97
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/backend/admin.py +6 -1
- package/backend/runtime.py +426 -416
- package/backend/schema.sql +6 -0
- package/backend/server.py +2372 -2359
- package/docs/deployment.md +3 -3
- package/docs/en/deployment.md +3 -3
- package/package.json +3 -2
- package/runner/admin_account.mjs +66 -0
- package/runner/codiscuss.mjs +3 -0
- package/runner/discussion_launch.mjs +3 -0
- package/runner/free_runner.mjs +4 -3
- package/web/app.js +41 -29
- package/web/markdown.js +5 -5
- package/web/modern.css +1 -1
- package/web/modern.js +2185 -2043
package/docs/deployment.md
CHANGED
|
@@ -25,7 +25,7 @@ python server.py --host 127.0.0.1 --port 8765 --no-demo
|
|
|
25
25
|
|
|
26
26
|
Python 3.10+,无第三方依赖。长期部署请自行配置进程管理与 HTTPS 反向代理;代理需对 `/api/updates` 关闭响应缓冲,以支持实时推送。
|
|
27
27
|
|
|
28
|
-
**管理员**:绑定 `127.0.0.1` 时访问者即管理员。绑定其他地址时,首次打开网站在左下角注册,需要安装令牌(环境变量 `DISCUSS_ADMIN_SETUP_KEY
|
|
28
|
+
**管理员**:绑定 `127.0.0.1` 时访问者即管理员。绑定其他地址时,首次打开网站在左下角注册,需要安装令牌(环境变量 `DISCUSS_ADMIN_SETUP_KEY`,未设置时随机生成并写入启动日志)。登录管理员后,在其他设备(平板、手机)上也能以用户身份在任意房间发言和管理;忘记密码时运行 `python server.py --reset-admin` 后重新注册。
|
|
29
29
|
|
|
30
30
|
**备份**:附件保存在数据库旁的 `<数据库文件名>.attachments/`。停止写入后同时备份数据库与附件目录。
|
|
31
31
|
|
|
@@ -95,11 +95,11 @@ Windows PowerShell 使用 `New-Item -ItemType Directory -Force -Path .wrangler/d
|
|
|
95
95
|
|
|
96
96
|
```sh
|
|
97
97
|
npx wrangler d1 execute DB --remote --config .wrangler/deploy/wrangler.jsonc --file backend/schema.sql
|
|
98
|
-
npx wrangler pages secret put ADMIN_SETUP_KEY --project-name YOUR_PROJECT_NAME
|
|
99
98
|
npm run deploy -- --cwd .wrangler/deploy
|
|
99
|
+
codiscuss admin token
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
`
|
|
102
|
+
`codiscuss admin token` 生成注册管理员用的一次性令牌(需已 `wrangler login`),在网站左下角注册时填入;在已用 `codiscuss local connect` 连上本机的网页注册时,账号密码会保存到 `~/.codiscuss/admin-accounts.json`。注册后其他设备直接用账号密码登录。`schema.sql` 可重复执行;升级已有站点前请先备份 D1(`npx wrangler d1 export`)。
|
|
103
103
|
|
|
104
104
|
发布后检查网页、`/api/health`,以及:
|
|
105
105
|
|
package/docs/en/deployment.md
CHANGED
|
@@ -25,7 +25,7 @@ python server.py --host 127.0.0.1 --port 8765 --no-demo
|
|
|
25
25
|
|
|
26
26
|
Python 3.10+ with no third-party dependencies. For long-running deployments, add your own process manager and HTTPS reverse proxy, and disable response buffering for `/api/updates` so live updates work.
|
|
27
27
|
|
|
28
|
-
**Administrator** — when bound to `127.0.0.1`, every visitor is the administrator. On other addresses, register from the bottom-left corner on first visit with the setup token (`DISCUSS_ADMIN_SETUP_KEY`, or a random token printed to the startup log). If you forget the password, run `python server.py --reset-admin` and register again.
|
|
28
|
+
**Administrator** — when bound to `127.0.0.1`, every visitor is the administrator. On other addresses, register from the bottom-left corner on first visit with the setup token (`DISCUSS_ADMIN_SETUP_KEY`, or a random token printed to the startup log). Once logged in as the administrator, you can speak and manage any room as the user from other devices such as a tablet or phone. If you forget the password, run `python server.py --reset-admin` and register again.
|
|
29
29
|
|
|
30
30
|
**Backup** — attachments are stored next to the database in `<database file>.attachments/`. Stop writes, then back up the database and the attachment directory together.
|
|
31
31
|
|
|
@@ -95,11 +95,11 @@ Keep the binding names `DB` and `ATTACHMENTS`. Without the R2 binding, text disc
|
|
|
95
95
|
|
|
96
96
|
```sh
|
|
97
97
|
npx wrangler d1 execute DB --remote --config .wrangler/deploy/wrangler.jsonc --file backend/schema.sql
|
|
98
|
-
npx wrangler pages secret put ADMIN_SETUP_KEY --project-name YOUR_PROJECT_NAME
|
|
99
98
|
npm run deploy -- --cwd .wrangler/deploy
|
|
99
|
+
codiscuss admin token
|
|
100
100
|
```
|
|
101
101
|
|
|
102
|
-
`
|
|
102
|
+
`codiscuss admin token` prints a one-time token for registering the administrator (requires `wrangler login`); enter it when registering from the bottom-left corner. Registering from a page connected with `codiscuss local connect` also saves the account to `~/.codiscuss/admin-accounts.json`. Other devices then simply log in with the username and password. `schema.sql` is safe to re-run; back up D1 (`npx wrangler d1 export`) before upgrading an existing site.
|
|
103
103
|
|
|
104
104
|
After publishing, check the page, `/api/health`, and:
|
|
105
105
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai_co_discuss",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.97",
|
|
4
4
|
"bin": {
|
|
5
5
|
"codiscuss": "runner/codiscuss.mjs"
|
|
6
6
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dev": "npm run build && wrangler pages dev --ip 127.0.0.1 --port 8787",
|
|
9
9
|
"db:local": "wrangler d1 execute DB --local --file backend/schema.sql",
|
|
10
10
|
"db:remote": "wrangler d1 execute DB --remote --file backend/schema.sql",
|
|
11
|
-
"check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_models.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs && node --check runner/discussion_api.mjs && node --check runner/api_participant.mjs && node --check runner/scheduler.mjs && node --check runner/remote_launch.mjs",
|
|
11
|
+
"check": "node --check backend/worker.mjs && node --check web/app.js && node --check runner/discussion_runner.mjs && node --check runner/discussion_adapters.mjs && node --check runner/discussion_sidecar.mjs && node --check runner/codex_app_client.mjs && node --check runner/discussion_service.mjs && node --check runner/codiscuss.mjs && node --check runner/discussion_launch.mjs && node --check runner/discussion_models.mjs && node --check runner/discussion_setup.mjs && node --check runner/discussion_skills.mjs && node --check runner/discussion_tui.mjs && node --check runner/discussion_local.mjs && node --check runner/free_runner.mjs && node --check runner/discussion_api.mjs && node --check runner/api_participant.mjs && node --check runner/scheduler.mjs && node --check runner/remote_launch.mjs && node --check runner/admin_account.mjs",
|
|
12
12
|
"runner": "node runner/discussion_runner.mjs",
|
|
13
13
|
"service": "node runner/discussion_service.mjs",
|
|
14
14
|
"build:ui": "vite build --config frontend/vite.config.mjs --mode markdown && vite build --config frontend/vite.config.mjs --mode backup && vite build --config frontend/vite.config.mjs",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"fflate": "0.8.3",
|
|
32
32
|
"katex": "0.18.10",
|
|
33
33
|
"markdown-it": "15.0.3",
|
|
34
|
+
"markdown-it-cjk-friendly": "3.0.0",
|
|
34
35
|
"motion-v": "2.7.0",
|
|
35
36
|
"pdfjs-dist": "6.4.299",
|
|
36
37
|
"vite": "8.3.4",
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Site administrator setup from the deploying computer: a one-time registration token and the saved account. */
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
|
|
9
|
+
import { atomic_json } from './discussion_adapters.mjs';
|
|
10
|
+
import { service_home } from './discussion_service.mjs';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_CONFIG = join('.wrangler', 'deploy', 'wrangler.jsonc');
|
|
13
|
+
const ACCOUNTS_FILE = 'admin-accounts.json';
|
|
14
|
+
const TOKEN_BYTES = 32;
|
|
15
|
+
const HELP = `用法:codiscuss admin token [--config WRANGLER_CONFIG]
|
|
16
|
+
|
|
17
|
+
token 生成一次性管理员注册令牌,经 wrangler 把它的哈希写入线上 D1,立即生效。
|
|
18
|
+
在线上网页左下角注册管理员时填入;注册成功即作废,重新运行会替换旧令牌。
|
|
19
|
+
需要这台电脑已 wrangler login;--config 默认为当前目录的 ${DEFAULT_CONFIG}。
|
|
20
|
+
|
|
21
|
+
在已用 codiscuss local connect 连上本机服务的网页注册后,账号与密码保存在 ~/.codiscuss/${ACCOUNTS_FILE}。
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
/** Write a fresh token's hash to the deployed D1 and return the token; the raw value is never stored. */
|
|
25
|
+
export function create_admin_token(config, run = run_wrangler) {
|
|
26
|
+
const path = resolve(config || DEFAULT_CONFIG);
|
|
27
|
+
if (!existsSync(path)) throw new Error(`找不到部署配置 ${path};请在项目目录运行,或用 --config 指定 wrangler.jsonc`);
|
|
28
|
+
const token = randomBytes(TOKEN_BYTES).toString('hex');
|
|
29
|
+
const hashed = createHash('sha256').update(token).digest('hex');
|
|
30
|
+
const directory = mkdtempSync(join(tmpdir(), 'codiscuss-admin-'));
|
|
31
|
+
const sql = join(directory, 'admin-setup.sql');
|
|
32
|
+
try {
|
|
33
|
+
writeFileSync(sql, `INSERT OR REPLACE INTO admin_setup (id,token_hash,created_at) VALUES (1,'${hashed}',${Math.floor(Date.now() / 1000)});\n`);
|
|
34
|
+
run(['d1', 'execute', 'DB', '--remote', '--config', path, '--file', sql]);
|
|
35
|
+
} finally { rmSync(directory, { recursive: true, force: true }); }
|
|
36
|
+
return { token, config: path };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Run wrangler through npx; arguments are quoted because Windows needs a shell for npx.cmd. */
|
|
40
|
+
function run_wrangler(args) {
|
|
41
|
+
const quoted = ['wrangler', ...args].map(arg => `"${arg.replaceAll('"', '\\"')}"`).join(' ');
|
|
42
|
+
const result = spawnSync(`npx ${quoted}`, { shell: true, stdio: ['ignore', 'ignore', 'pipe'], encoding: 'utf8', windowsHide: true });
|
|
43
|
+
if (result.status !== 0) throw new Error(`wrangler 写入线上数据库失败:${(result.stderr || result.error?.message || '').trim().slice(-800)}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep the account registered from this computer's browser, one entry per site, in the private service directory. */
|
|
47
|
+
export function save_admin_account(home, { site, username, password }) {
|
|
48
|
+
if (typeof username !== 'string' || !username.trim() || typeof password !== 'string' || !password) throw new Error('缺少管理员用户名或密码');
|
|
49
|
+
const origin = new URL(site).origin;
|
|
50
|
+
const path = join(home, ACCOUNTS_FILE);
|
|
51
|
+
const accounts = existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : { sites: {} };
|
|
52
|
+
accounts.sites[origin] = { username: username.trim(), password, saved_at: new Date().toISOString() };
|
|
53
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
54
|
+
atomic_json(path, accounts);
|
|
55
|
+
return { saved: path };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `codiscuss admin ...` */
|
|
59
|
+
export function admin_main(args) {
|
|
60
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
|
|
61
|
+
config: { type: 'string' }, help: { type: 'boolean', short: 'h' },
|
|
62
|
+
} });
|
|
63
|
+
if (values.help || positionals[0] !== 'token') return process.stdout.write(HELP);
|
|
64
|
+
const { token } = create_admin_token(values.config);
|
|
65
|
+
process.stdout.write(`管理员注册令牌(一次性):\n${token}\n\n在线上网页左下角注册管理员时填入。注册成功后作废;账号密码在已连接本机服务的网页注册时保存到 ${join(service_home(), ACCOUNTS_FILE)}。\n`);
|
|
66
|
+
}
|
package/runner/codiscuss.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
|
|
7
|
+
import { admin_main } from './admin_account.mjs';
|
|
7
8
|
import { main as service_main, read_registry, run_service, service_home, service_pid } from './discussion_service.mjs';
|
|
8
9
|
import { launch_main } from './discussion_launch.mjs';
|
|
9
10
|
import { local_settings, setup_main } from './discussion_setup.mjs';
|
|
@@ -16,6 +17,7 @@ const HELP = `AI CoDiscuss CLI
|
|
|
16
17
|
(无命令) 在当前终端启动服务,Ctrl+C 停止
|
|
17
18
|
install / setup 安装持久命令并配置 / 配置已有安装
|
|
18
19
|
local COMMAND 管理本机网站,或 connect 连接线上站点
|
|
20
|
+
admin token 生成线上网站的一次性管理员注册令牌
|
|
19
21
|
service COMMAND 启动、停止、查询服务,或登记房间
|
|
20
22
|
launch COMMAND 保存启动配置,或从 AI 对话启动讨论
|
|
21
23
|
room COMMAND 创建、读取或更新讨论房间
|
|
@@ -118,6 +120,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
118
120
|
if (command === 'launch') return launch_main(args.slice(1));
|
|
119
121
|
if (['install', 'setup'].includes(command)) return setup_main(command, args.slice(1));
|
|
120
122
|
if (command === 'local') return local_main(args.slice(1));
|
|
123
|
+
if (command === 'admin') return admin_main(args.slice(1));
|
|
121
124
|
if (!command || (command === 'run' && args.length === 1)) return service_cli([]);
|
|
122
125
|
if (SERVICE_COMMANDS.has(command) || command === '--json') return service_cli(args);
|
|
123
126
|
return website_cli(args);
|
|
@@ -60,6 +60,7 @@ const HELP = `用法:codiscuss launch settings [--file FILE]
|
|
|
60
60
|
codiscuss launch projects [--file FILE]
|
|
61
61
|
codiscuss launch presets [--file FILE]
|
|
62
62
|
codiscuss launch machine-link --file FILE
|
|
63
|
+
codiscuss launch admin-account --file FILE
|
|
63
64
|
|
|
64
65
|
settings 读取本机软件默认配置;指定文件时保存,不启动 AI。
|
|
65
66
|
start 使用启动 JSON 创建或接入房间并启动;--dry-run 只校验和预览。
|
|
@@ -73,6 +74,7 @@ pick-directory 在本机弹出系统目录选择窗口,返回所选路径;
|
|
|
73
74
|
projects 读取本机已配置的项目(名称与绝对路径);提供 JSON(projects 列表)时整体替换保存。
|
|
74
75
|
presets 读取本机的专家预设(含内置预设)与各平台默认描述;提供 JSON(presets 列表和/或 description_templates)时替换对应部分。
|
|
75
76
|
machine-link 返回本机连接指定 site 的电脑编号、名称与凭据,供已登录管理员的网页完成配对;不存在时创建。
|
|
77
|
+
admin-account 把在指定 site 注册的管理员用户名与密码保存到服务目录的 admin-accounts.json;须提供 site、username、password。
|
|
76
78
|
room-member 为运行中的自由房间或讨论邀请、请回一位成员(讨论只接受本机唤醒的成员);须提供 site、room_id、token、action(add/rejoin)、name。
|
|
77
79
|
room-member-access 修改一位本机唤醒成员的项目文件权限(project_access 为 read/write),下次投递起生效、保留原会话;须提供 site、room_id、token、name、project_access。
|
|
78
80
|
room-member-adapter 把自由房间里空闲的 Codex 成员在 App(codex)与 CLI(codex_cli)间切换,保留原会话;须提供 site、room_id、token、name、adapter。
|
|
@@ -1111,6 +1113,7 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1111
1113
|
else if (positionals[0] === 'room-restore' && input) result = await restore_room(home, input);
|
|
1112
1114
|
else if (positionals[0] === 'pick-directory') result = await pick_directory();
|
|
1113
1115
|
else if (positionals[0] === 'machine-link' && input) result = (await import('./remote_launch.mjs')).machine_link(home, input.site);
|
|
1116
|
+
else if (positionals[0] === 'admin-account' && input) result = (await import('./admin_account.mjs')).save_admin_account(home, input);
|
|
1114
1117
|
else if (positionals[0] === 'start' && input) result = await launch_discussion(home, input, { dry_run: values['dry-run'], browser_request: values.browser });
|
|
1115
1118
|
else if (positionals[0] === 'room-config' && input) result = await room_configuration(home, input, { local_guard: values['configuration-locked'] });
|
|
1116
1119
|
else throw new Error(HELP);
|
package/runner/free_runner.mjs
CHANGED
|
@@ -245,7 +245,8 @@ export class FreeRoomRunner {
|
|
|
245
245
|
}
|
|
246
246
|
/** Ask the scheduler how everyone answers; a failed call still has them answer at once. */
|
|
247
247
|
async order_group(request, room, everyone) {
|
|
248
|
-
|
|
248
|
+
// A page holds at most 20 messages, so the window must end at the message itself or the message falls off the page.
|
|
249
|
+
const page = await api(this.config, `/messages?after=${Math.max(0, request.message_id - ROUTE_CONTEXT_MESSAGES)}`);
|
|
249
250
|
const message = page.messages.find(item => item.id === request.message_id);
|
|
250
251
|
const members = (room.speaking_order?.items || []).map(item => item.name).filter(name => everyone.some(member => member.name === name));
|
|
251
252
|
const decision = await decide_group_order(this.config.api_home, this.config.scheduler,
|
|
@@ -282,9 +283,9 @@ export class FreeRoomRunner {
|
|
|
282
283
|
}
|
|
283
284
|
/** Decide one message and apply a confident pick: one member, everyone, or no one; otherwise the host keeps it. */
|
|
284
285
|
async recommend(request, room, everyone) {
|
|
285
|
-
const page = await api(this.config, `/messages?after=${Math.max(0, request.message_id - ROUTE_CONTEXT_MESSAGES
|
|
286
|
+
const page = await api(this.config, `/messages?after=${Math.max(0, request.message_id - ROUTE_CONTEXT_MESSAGES)}`);
|
|
286
287
|
const message = page.messages.find(item => item.id === request.message_id);
|
|
287
|
-
if (!message)
|
|
288
|
+
if (!message) throw new Error('读取不到这条消息');
|
|
288
289
|
const described = this.route_members(everyone, room, message.id);
|
|
289
290
|
const host = described.find(member => member.role === 'host');
|
|
290
291
|
const members = described.filter(member => member !== host);
|
package/web/app.js
CHANGED
|
@@ -70,6 +70,8 @@ state.archive_changes = new Map();
|
|
|
70
70
|
state.archive_version = 0;
|
|
71
71
|
state.directory_error = "";
|
|
72
72
|
state.directory_notice = "";
|
|
73
|
+
// The open import's progress for the import dialog: { done, rooms: [{ title, status, members, error }], errors }.
|
|
74
|
+
state.import_progress = null;
|
|
73
75
|
state.directory_pending = new Map();
|
|
74
76
|
state.directory_version = 0;
|
|
75
77
|
state.navigation_version = 0;
|
|
@@ -230,10 +232,15 @@ function accept_admin_session({ token, profile }) {
|
|
|
230
232
|
|
|
231
233
|
/** Create the single administrator account and sign in with it. */
|
|
232
234
|
async function register_admin({ username, password, confirm, setup_key }) {
|
|
233
|
-
const problem = validate_admin_form({ username, password, confirm }) || (String(setup_key ?? "").trim() ? "" : "
|
|
235
|
+
const problem = validate_admin_form({ username, password, confirm }) || (String(setup_key ?? "").trim() ? "" : "请填写注册令牌");
|
|
234
236
|
if (problem) throw new Error(problem);
|
|
235
237
|
try { accept_admin_session(await request_json("/api/admin/register", { username: username.trim(), password, setup_key: setup_key.trim() })); }
|
|
236
238
|
catch (error) { throw admin_error(error); }
|
|
239
|
+
// Registered from the computer running the local service: keep the account in its private directory.
|
|
240
|
+
if (local_connection) {
|
|
241
|
+
try { show_toast(`账号与密码已保存到本机 ${(await local_launch_request("admin-account", { username: username.trim(), password })).saved}`); }
|
|
242
|
+
catch (error) { show_toast(`注册成功,但账号未能保存到本机:${error.message}`, true); }
|
|
243
|
+
}
|
|
237
244
|
}
|
|
238
245
|
|
|
239
246
|
/** Sign in and keep the session in this browser. */
|
|
@@ -289,6 +296,11 @@ async function admin_avatar_from_file(file) {
|
|
|
289
296
|
} finally { URL.revokeObjectURL(url); }
|
|
290
297
|
}
|
|
291
298
|
|
|
299
|
+
/** Whether this browser may write as the room owner: its own control credential, or a logged-in administrator session. */
|
|
300
|
+
function has_room_control(room_id) {
|
|
301
|
+
return Boolean(localStorage.getItem(`discuss-access:${room_id}`) || (state.admin.logged_in && admin_token()));
|
|
302
|
+
}
|
|
303
|
+
|
|
292
304
|
/** Explain why a management write failed when this browser has neither credential. */
|
|
293
305
|
function management_error_message(room_id, error) {
|
|
294
306
|
if (error.status === 403 && !state.admin.logged_in && !localStorage.getItem(`discuss-access:${room_id}`)) return MANAGEMENT_LOGIN_HINT;
|
|
@@ -1691,7 +1703,7 @@ function access_snapshot() {
|
|
|
1691
1703
|
const room = state.room;
|
|
1692
1704
|
if (!room) return { can_manage: false, members: [], notice: "", error: "" };
|
|
1693
1705
|
const saved = member_credentials(room.id);
|
|
1694
|
-
return { can_manage:
|
|
1706
|
+
return { can_manage: has_room_control(room.id),
|
|
1695
1707
|
members: (room.members || room.free?.members || []).filter(member => member.status !== "removed").map(member => ({
|
|
1696
1708
|
name: member.name, access: member.access, available: member.status === "active" && member.access !== "online"
|
|
1697
1709
|
&& Boolean(saved[member.name]?.token) && (saved[member.name].access === "online") === (member.access === "online")
|
|
@@ -2606,7 +2618,7 @@ async function choose_runtime(mode) {
|
|
|
2606
2618
|
const path = `/api/rooms/${room_id}/runtime`;
|
|
2607
2619
|
const runtime = await request_json(path);
|
|
2608
2620
|
if (runtime.mode !== mode) {
|
|
2609
|
-
if (!
|
|
2621
|
+
if (!has_room_control(room_id)) throw new Error(MANAGEMENT_LOGIN_HINT);
|
|
2610
2622
|
await request_json(path, { action: "configure", mode, revision: runtime.revision });
|
|
2611
2623
|
}
|
|
2612
2624
|
if (state.room?.id !== room_id) return;
|
|
@@ -2710,7 +2722,7 @@ async function conclude_discussion() {
|
|
|
2710
2722
|
/** Turn this free room into a discussion within the given limits and start the run; the local service takes it over at its next check. */
|
|
2711
2723
|
async function start_room_discussion({ goal, max_rounds, max_minutes, execution_minutes = 0, named_followup = false }) {
|
|
2712
2724
|
const room_id = state.room.id, path = `/api/rooms/${room_id}`;
|
|
2713
|
-
if (!
|
|
2725
|
+
if (!has_room_control(room_id)) throw new Error(MANAGEMENT_LOGIN_HINT);
|
|
2714
2726
|
await request_json(`${path}/discussion`, { action: "start", ...(goal ? { goal } : {}), limits: { max_rounds, max_minutes, execution_minutes }, named_followup });
|
|
2715
2727
|
const runtime = await request_json(`${path}/runtime`);
|
|
2716
2728
|
await request_json(`${path}/runtime`, { action: "start", mode: "automatic", revision: runtime.revision, deadline: 0 });
|
|
@@ -3447,7 +3459,7 @@ function view_snapshot() {
|
|
|
3447
3459
|
const experts = Object.fromEntries(["new-expert-modes", "topic-expert-modes"].map((id) =>
|
|
3448
3460
|
[id, element(id) ? read_expert_inputs(id) : {}]));
|
|
3449
3461
|
return { rooms: ordered_rooms(), folders: state.folders, folder_groups: ordered_folders(), room: state.room, archive_view: state.archive_view, directory_sort: state.directory_sort, messages: state.messages, total: state.message_total,
|
|
3450
|
-
archive_pending: [...state.archive_changes.keys()], directory_error: state.directory_error, directory_notice: state.directory_notice,
|
|
3462
|
+
archive_pending: [...state.archive_changes.keys()], directory_error: state.directory_error, directory_notice: state.directory_notice, import_progress: structuredClone(state.import_progress),
|
|
3451
3463
|
directory_pending: [...state.directory_pending.keys()], folder_pending: [...state.folder_moves.keys()], creating_room: state.creating_room,
|
|
3452
3464
|
connected: state.connected, backup_pending: Boolean(state.backup_pending), page: state.page, pages: state.pages, query: state.query,
|
|
3453
3465
|
attachment_drafts: state.attachment_drafts.map(item => ({ id: item.id, file_name: item.file.name, size: item.file.size, preview_url: item.preview_url })),
|
|
@@ -3586,17 +3598,15 @@ function import_body(snapshot) {
|
|
|
3586
3598
|
return { ...body, tables: { ...body.tables, room_members } };
|
|
3587
3599
|
}
|
|
3588
3600
|
|
|
3589
|
-
/** Register an imported room's exported members on this machine; returns
|
|
3601
|
+
/** Register an imported room's exported members on this machine; returns one short line per member and any room-wide problem. */
|
|
3590
3602
|
async function restore_local_members(room, token, local_members) {
|
|
3591
|
-
if (!state.launch.available) return {
|
|
3603
|
+
if (!state.launch.available) return { members: [], problem: "未连接本机服务,成员会话未恢复" };
|
|
3592
3604
|
try {
|
|
3593
3605
|
const result = await local_launch_request("room-restore", { room_id: room.id, token, local_members });
|
|
3594
|
-
|
|
3595
|
-
?
|
|
3596
|
-
:
|
|
3597
|
-
|
|
3598
|
-
return { notices: notices.map(text => `${room.title}:${text}`), problems: problems.map(text => `${room.title}:${text}`) };
|
|
3599
|
-
} catch (error) { return { notices: [], problems: [`${room.title}:成员会话未恢复:${error.message}`] }; }
|
|
3606
|
+
return { members: result.members.map(member => ({ name: member.name, failed: member.status === "failed",
|
|
3607
|
+
text: member.status === "failed" ? `未恢复:${member.error}` : member.status === "resumed" ? "请回后接续原会话" : "已改为手动接入,请回后在原窗口重新注册",
|
|
3608
|
+
resume: member.status === "manual" && member.session_id ? { command: `claude --resume ${member.session_id}`, cwd: member.cwd } : null })) };
|
|
3609
|
+
} catch (error) { return { members: [], problem: `成员会话未恢复:${error.message}` }; }
|
|
3600
3610
|
}
|
|
3601
3611
|
|
|
3602
3612
|
/** Upload one imported room's original files to the new attachment IDs; returns how many were missing. */
|
|
@@ -3615,41 +3625,43 @@ async function upload_imported_files(room_id, token, mapping, files) {
|
|
|
3615
3625
|
async function import_rooms({ files, folder_id = null }) {
|
|
3616
3626
|
if (!files?.length || state.backup_pending) return;
|
|
3617
3627
|
state.backup_pending = true;
|
|
3618
|
-
|
|
3628
|
+
const progress = state.import_progress = { done: false, rooms: [], errors: [] };
|
|
3619
3629
|
publish_view();
|
|
3620
|
-
const imported = [], skipped = [], failed = [], restored = [], unrestored = [], unfiled = [];
|
|
3621
|
-
let missing = 0;
|
|
3622
3630
|
try {
|
|
3623
3631
|
for (const file of files) {
|
|
3624
3632
|
let rooms;
|
|
3625
3633
|
try { rooms = await window.discuss_backup.read_room_archives(file); }
|
|
3626
|
-
catch (error) {
|
|
3634
|
+
catch (error) { progress.errors.push(error.message.includes(file.name) ? error.message : `${file.name}:${error.message}`); publish_view(); continue; }
|
|
3627
3635
|
for (const { snapshot, files: originals } of rooms) {
|
|
3628
|
-
const
|
|
3636
|
+
const item = { title: snapshot.tables?.rooms?.[0]?.title || file.name, status: "importing", members: [], problems: [] };
|
|
3637
|
+
progress.rooms.push(item);
|
|
3638
|
+
publish_view();
|
|
3639
|
+
const token = crypto.randomUUID() + crypto.randomUUID();
|
|
3629
3640
|
try {
|
|
3630
3641
|
const result = await request_json("/api/rooms/import", import_body(snapshot), token);
|
|
3631
|
-
|
|
3642
|
+
item.title = result.title;
|
|
3643
|
+
if (result.skipped) { item.status = "skipped"; continue; }
|
|
3632
3644
|
save_room_credentials({ id: result.id }, token);
|
|
3633
|
-
missing
|
|
3634
|
-
|
|
3645
|
+
const missing = await upload_imported_files(result.id, token, result.attachments, originals);
|
|
3646
|
+
if (missing) item.problems.push(`${missing} 个附件原文件缺失,消息中的这些附件无法打开`);
|
|
3635
3647
|
if (folder_id) await request_json(`/api/rooms/${result.id}/folder`, { actor: "用户", folder_id })
|
|
3636
|
-
.catch(error =>
|
|
3648
|
+
.catch(error => item.problems.push(`未放入文件夹:${error.message}`));
|
|
3637
3649
|
if (snapshot.local_members) {
|
|
3638
|
-
const {
|
|
3639
|
-
|
|
3650
|
+
const { members, problem } = await restore_local_members(result, token, snapshot.local_members);
|
|
3651
|
+
item.members = members;
|
|
3652
|
+
if (problem) item.problems.push(problem);
|
|
3640
3653
|
}
|
|
3641
|
-
|
|
3654
|
+
item.status = "imported";
|
|
3655
|
+
} catch (error) { Object.assign(item, { status: "failed", error: error.message }); }
|
|
3656
|
+
finally { publish_view(); }
|
|
3642
3657
|
}
|
|
3643
3658
|
}
|
|
3644
3659
|
await refresh_room();
|
|
3645
3660
|
} finally {
|
|
3661
|
+
progress.done = true;
|
|
3646
3662
|
state.backup_pending = false;
|
|
3647
3663
|
publish_view();
|
|
3648
3664
|
}
|
|
3649
|
-
state.directory_notice = [`已导入 ${imported.length} 个频道`, skipped.length && `跳过已存在的 ${skipped.length} 个(${skipped.join("、")})`, ...restored]
|
|
3650
|
-
.filter(Boolean).join(";");
|
|
3651
|
-
state.directory_error = [missing && `${missing} 个附件原文件缺失,消息中的这些附件无法打开`, failed.length && `未导入 ${failed.length} 项:${failed.join(";")}`, ...unfiled, ...unrestored]
|
|
3652
|
-
.filter(Boolean).join(";");
|
|
3653
3665
|
}
|
|
3654
3666
|
|
|
3655
3667
|
/** Execute a named view command, retaining existing retry and provenance rules. */
|