ai_co_discuss 0.3.35 → 0.3.38

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 CHANGED
@@ -30,7 +30,7 @@ Most multi-agent frameworks spin up fresh agents inside their own runtime. AI Co
30
30
  - **Structured discussions** — set a goal and round / time limits; the host schedules speakers and follow-ups, then the room returns to free collaboration with history and conclusions kept.
31
31
  - **Smart Scheduling** — optionally let a decision model handle routine routing instead of waking the host.
32
32
  - **Mix any participant** — local CLIs and desktop apps, model APIs and manually connected sessions in one room.
33
- - **Shared workspace** — point a room at a project directory and grant each local agent read-only or writable access.
33
+ - **Shared workspace** — point a room at a project directory, grant each local agent read-only or writable access, and browse files with text, Markdown and static HTML previews from the room.
34
34
  - **Live roster** — invite, pause, recall or remove members at any time, including resuming an existing agent session.
35
35
  - **Portable records** — attachments, ZIP export / import between sites with discussion history, and sourced reports via the `discuss-report` skill.
36
36
  - **Local first** — runs entirely on your machine; optionally host the site on Cloudflare or Docker while agents stay local.
package/README.zh-CN.md CHANGED
@@ -30,7 +30,7 @@
30
30
  - **结构化讨论**:设定目标与轮次 / 时长上限,主持人安排发言与追问;结束后回到自由协作,记录与结论保留。
31
31
  - **智能调度**:可选由决策模型处理日常分派,不必每次唤醒主持人。
32
32
  - **混合接入**:本地 CLI 与桌面 App、模型 API、手动接入的会话可同房协作。
33
- - **共享项目**:为房间指定项目目录,并为每位本地 AI 设置只读或可写。
33
+ - **共享项目**:为房间指定项目目录,并为每位本地 AI 设置只读或可写。房间“文件”页可浏览目录,悬浮预览文本、Markdown 和静态 HTML。
34
34
  - **随时调整成员**:随时邀请、暂离、请回或移除成员,也可以续接一个已有的 AI 会话。
35
35
  - **可迁移记录**:附件、包含讨论历史的跨站点 ZIP 导出 / 导入,并可用 `discuss-report` 技能生成带来源的报告。
36
36
  - **本机优先**:完全在本机运行;也可将网站部署到 Cloudflare 或 Docker,AI 仍在本机执行。
package/backend/server.py CHANGED
@@ -1743,7 +1743,7 @@ class DiscussionHandler(BaseHTTPRequestHandler):
1743
1743
  """Forward authenticated browser requests to the shared Node entrypoint and pin their website."""
1744
1744
  site = self.local_request_site(method)
1745
1745
  route = parts[2:]
1746
- if method == "OPTIONS" and route in (["settings"], ["launch"], ["room-models"], ["room-member"], ["room-member-access"], ["room-member-allow"], ["room-member-adapter"], ["room-member-model"], ["room-profiles"], ["room-restore"], ["room-fork"], ["models"], ["api-key"], ["api-check"],
1746
+ if method == "OPTIONS" and route in (["settings"], ["launch"], ["room-models"], ["room-files"], ["room-member"], ["room-member-access"], ["room-member-allow"], ["room-member-adapter"], ["room-member-model"], ["room-profiles"], ["room-restore"], ["room-fork"], ["models"], ["api-key"], ["api-check"],
1747
1747
  ["scheduler-check"], ["tts-settings"], ["tts"], ["stt"], ["pick-directory"], ["projects"], ["presets"], ["machine-link"], ["admin-account"]):
1748
1748
  return self.respond({"ok": True})
1749
1749
  if route == ["settings"] and method in ("GET", "POST"):
@@ -1766,7 +1766,7 @@ class DiscussionHandler(BaseHTTPRequestHandler):
1766
1766
  command = route[0]
1767
1767
  elif route in (["room-member"], ["room-member-access"], ["room-member-allow"], ["room-member-adapter"], ["room-member-model"]) and method == "POST":
1768
1768
  command = route[0]
1769
- elif route in (["room-profiles"], ["room-restore"], ["room-fork"]) and method == "POST":
1769
+ elif route in (["room-profiles"], ["room-files"], ["room-restore"], ["room-fork"]) and method == "POST":
1770
1770
  command = route[0]
1771
1771
  elif route == ["pick-directory"] and method == "POST":
1772
1772
  command = "pick-directory"
@@ -1785,9 +1785,13 @@ class DiscussionHandler(BaseHTTPRequestHandler):
1785
1785
  if self.headers.get_content_type() != "application/json":
1786
1786
  raise ValueError("本机操作须使用 application/json")
1787
1787
  payload = self.read_payload()
1788
- if command in ("start", "room-config", "room-models", "room-member", "room-member-access", "room-member-allow", "room-member-adapter", "room-member-model", "room-profiles", "room-restore", "room-fork"):
1788
+ if command in ("start", "room-config", "room-models", "room-files", "room-member", "room-member-access", "room-member-allow", "room-member-adapter", "room-member-model", "room-profiles", "room-restore", "room-fork"):
1789
1789
  payload.pop("access_file", None)
1790
1790
  payload.update(site=site, entry="web")
1791
+ if command == "room-files" and not payload.get("token") and not self.bridge_origin:
1792
+ with closing(connect_db(self.server.database)) as connection:
1793
+ self.admin_as_owner(connection, payload.get("room_id", ""))
1794
+ payload["token"] = self.headers.get("X-Discussion-Token", "")
1791
1795
  if command == "machine-link":
1792
1796
  payload = {"site": site}
1793
1797
  if command == "admin-account":
package/docs/en/runner.md CHANGED
@@ -37,6 +37,12 @@ codiscuss setup
37
37
 
38
38
  Run `codiscuss local open`, click **New Room** (新建房间), add members and pick a host. If a launch fails, refresh the page to continue the original request without creating a duplicate room.
39
39
 
40
+ ### Browse Project Files
41
+
42
+ When a room has a project directory, its **Files** page below **Room** lists that computer's files and folders, sizes and modification times. Click a file to open a floating text preview, with rendered Markdown and static HTML views. Rooms without a project directory show a disabled entry.
43
+
44
+ Other devices read the same directory over the paired computer's encrypted connection. Previews include up to 128 KB and support UTF-8 and BOM-marked UTF-16. HTML previews display inline styles and embedded images. Access stays within the room's project directory.
45
+
40
46
  ### From an AI Chat
41
47
 
42
48
  In the local agent that should host, invoke `discuss-host` and `discuss-participant` and describe the site, topic, participants and limits. The host skill creates the room with your local configuration, and the current session keeps hosting. `agy` cannot host a fully automatic discussion from its own chat.
@@ -91,6 +97,7 @@ Permissions are set per automatically started CLI member and default to read-onl
91
97
  | Open the local site | `codiscuss local open` |
92
98
  | Show status | `codiscuss local status` |
93
99
  | Stop | `codiscuss local stop` |
100
+ | Restart the discussion service (picks up new environment variables; run it from a terminal that has them) | `codiscuss service restart` |
94
101
  | Start at login (Windows) | `codiscuss service autostart on` |
95
102
 
96
103
  ## Command Line
package/docs/runner.md CHANGED
@@ -37,6 +37,12 @@ codiscuss setup
37
37
 
38
38
  运行 `codiscuss local open`,点击“新建房间”,添加成员并指定主持人。启动失败时刷新页面,可沿用原请求继续,不会重复建房。
39
39
 
40
+ ### 浏览项目文件
41
+
42
+ 房间设置了项目目录后,左栏“房间”下方的“文件”页可浏览电脑上的文件夹与文件,查看大小和修改时间。点击文件打开悬浮预览,支持文本、Markdown 渲染与 HTML 静态渲染,并可切换源码。未设置项目目录时入口灰显。
43
+
44
+ 其他设备通过已配对电脑的加密连接读取同一目录。文件内容最多预览前 128 KB,支持 UTF-8 和带 BOM 的 UTF-16;HTML 展示内联样式与内嵌图片。文件访问限于房间项目目录,目录外的链接灰显。
45
+
40
46
  ### 从 AI 对话发起
41
47
 
42
48
  在要担任主持人的本地 AI 对话中调用 `discuss-host` 和 `discuss-participant`,说明站点、议题、参与者与上限。主持技能沿用本机软件配置建房,当前会话继续主持。agy 不能在自己的对话中主持全自动讨论。
@@ -110,6 +116,7 @@ API 成员可以担任专家或主持人。房间指定了项目目录时,它
110
116
  | 打开本机网站 | `codiscuss local open` |
111
117
  | 查看状态 | `codiscuss local status` |
112
118
  | 停止 | `codiscuss local stop` |
119
+ | 重启讨论服务(使新环境变量生效,须在已有新值的终端执行) | `codiscuss service restart` |
113
120
  | 登录时自动启动(Windows) | `codiscuss service autostart on` |
114
121
 
115
122
  ## 命令行
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.3.35",
3
+ "version": "0.3.38",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -10,6 +10,7 @@ import { pathToFileURL } from 'node:url';
10
10
  import { parseArgs } from 'node:util';
11
11
 
12
12
  import { atomic_json } from './files.mjs';
13
+ import { read_project_files } from './project_files.mjs';
13
14
  import { setup_sidecar } from './adapters/antigravity_app.mjs';
14
15
  import { adapter_of, adoption_adapter, BRANCHABLE_ADAPTERS, branch_session, session_settings, software_defaults, software_effort, software_models } from './adapters/index.mjs';
15
16
  import { ADAPTER_IDS, ADAPTER_PROFILES, adapter_profile, platform_adapters, same_session_family } from './adapters/profiles.mjs';
@@ -54,6 +55,7 @@ const HELP = `用法:codiscuss launch settings [--file FILE]
54
55
  codiscuss launch start --file FILE [--dry-run]
55
56
  codiscuss launch room-config --file FILE
56
57
  codiscuss launch room-models --file FILE
58
+ codiscuss launch room-files --file FILE
57
59
  codiscuss launch models --file FILE
58
60
  codiscuss launch api-key --file FILE
59
61
  codiscuss launch api-check --file FILE
@@ -79,6 +81,7 @@ settings 读取本机软件默认配置;指定文件时保存,不启动 AI
79
81
  start 使用启动 JSON 创建或接入房间并启动;--dry-run 只校验和预览。
80
82
  room-config 修正已暂停房间的软件路径或切换 Codex App / CLI,不启动讨论;须提供 site、room_id、token。
81
83
  room-models 只读指定 site、room_id 的本机登记模型与思考强度,不启动讨论;quick 为 true 时不查询 agy 的默认模型(较慢),该成员标 defaults_pending。
84
+ room-files 只读房间已保存的项目目录,须提供 site、room_id、token,可选相对 path 和目录分页 offset;文件返回最多前 128 KiB 文本。
82
85
  models 按 adapter、executable 或 API profile 读取模型目录,不启动讨论或生成回答。
83
86
  api-key 将 base_url、api_key 保存于本机私有目录,只返回 api_key_ref。
84
87
  api-check 用 API profile 向模型发送一次极短请求,核对地址、密钥、协议与模型是否可用。
@@ -303,7 +306,13 @@ export async function room_models(home, raw, { read_defaults = software_defaults
303
306
  })));
304
307
  // Smart scheduling is a local choice; only which model decides is shown, never its key.
305
308
  const scheduler = config.scheduler ? { provider: config.scheduler.provider, model: config.scheduler.model, mode: config.scheduler.mode || DEFAULT_SCHEDULER_MODE } : null;
306
- return { room_id: input.room_id, registered: paths.length > 0, participants, scheduler };
309
+ return { room_id: input.room_id, registered: paths.length > 0, participants, scheduler, project_directory: config.cwd || '' };
310
+ }
311
+
312
+ /** Read the owner's saved room project, never a directory supplied by the request. */
313
+ export async function room_files(home, raw) {
314
+ const { input, config } = await member_room_context(home, raw);
315
+ return { room_id: input.room_id, ...read_project_files(config.cwd, input.path, input.offset) };
307
316
  }
308
317
 
309
318
  /** Save only reusable client settings; room credentials and session bindings are never defaults. */
@@ -1308,6 +1317,7 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
1308
1317
  else if (positionals[0] === 'tts' && input) result = await tts_speak(home, input);
1309
1318
  else if (positionals[0] === 'stt' && input) result = await stt_part(home, input);
1310
1319
  else if (positionals[0] === 'room-models' && input) result = await room_models(home, input);
1320
+ else if (positionals[0] === 'room-files' && input) result = await room_files(home, input);
1311
1321
  else if (positionals[0] === 'models' && adapter_profile(input?.adapter)?.kind === 'api') result = await api_models(home, input);
1312
1322
  else if (positionals[0] === 'models' && input) {
1313
1323
  const saved = Object.values(launch_settings(home).participants).find(profile => profile.adapter === input.adapter);
@@ -1,4 +1,4 @@
1
- /** Own the local website and only the discussion service started by this supervisor. */
1
+ /** Own the local website and the discussion service it starts; local stop also stops a pre-existing service. */
2
2
  import { spawn } from 'node:child_process';
3
3
  import { createHash, randomBytes } from 'node:crypto';
4
4
  import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync } from 'node:fs';
@@ -226,16 +226,15 @@ export function restart_reason(home = service_home()) {
226
226
  return null;
227
227
  }
228
228
 
229
- /** Ask the owned supervisor to stop, retaining its database and user configuration. */
229
+ /** Stop the supervisor and any discussion service, retaining the database and user configuration. */
230
230
  async function stop_local(home) {
231
- if (!local_status(home).pid) return local_status(home);
232
- const path = join(home, 'local', 'stop.request');
233
- atomic_json(path, { requested_at: Date.now() });
234
- for (let waited = 0; waited < STOP_WAIT_MS; waited += POLL_MS) {
235
- if (!local_status(home).pid) return local_status(home);
236
- await sleep(POLL_MS);
231
+ if (local_status(home).pid) {
232
+ atomic_json(join(home, 'local', 'stop.request'), { requested_at: Date.now() });
233
+ for (let waited = 0; waited < STOP_WAIT_MS && local_status(home).pid; waited += POLL_MS) await sleep(POLL_MS);
234
+ if (local_status(home).pid) throw new Error('本机服务尚未停止,请查看 local/supervisor.log');
237
235
  }
238
- throw new Error('本机服务尚未停止,请查看 local/supervisor.log');
236
+ if (service_pid(home)) await service_main(['stop'], () => {});
237
+ return local_status(home);
239
238
  }
240
239
 
241
240
  /** Open the supervisor's loopback URL or a website explicitly authorized by local connect. */
@@ -258,8 +257,7 @@ function local_summary(command, result, opened) {
258
257
  status: result.running ? [`本机网站运行中:${site}`]
259
258
  : result.website_running ? [`本机网站运行中:${site},但讨论服务未运行;运行 codiscuss local open 恢复`]
260
259
  : ['本机网站未运行;运行 codiscuss local open 启动'],
261
- stop: result.service_pid ? [`本机网站已停止;独立讨论服务仍在运行(PID ${result.service_pid}),可用 codiscuss service stop 单独停止;数据已保留`]
262
- : ['本机网站与讨论服务已停止;数据已保留,运行 codiscuss local open 重新启动'],
260
+ stop: ['本机网站与讨论服务已停止;数据已保留,运行 codiscuss local open 重新启动'],
263
261
  connect: [`已连接 ${result.connected};本机网站 ${tone.cyan(result.local_url || '')}`,
264
262
  opened ? '已在浏览器打开连接链接,请在该页面完成接入;连接链接不要转发' : `请在浏览器打开连接链接完成接入,链接不要转发:${result.url}`],
265
263
  disconnect: [`已撤销 ${result.disconnected} 的本机启动权限;正在运行的讨论需在进度看板暂停`],
@@ -43,7 +43,7 @@ const REPO_ROOT = dirname(dirname(SCRIPT));
43
43
  const SHORTCUT_NAME = 'AI CoDiscuss 讨论服务.lnk';
44
44
  const TICK_STATES = { undefined: 'running', waiting: 'paused', false: 'idle' };
45
45
  const STATUS_LABELS = { running: '运行中', paused: '已暂停', idle: '等待启动', error: '异常' };
46
- const USAGE = '用法: discussion_service.mjs start|stop|status|open|run|list|add|remove|clean|shortcut|autostart [--config 私有配置.json] [on|off]';
46
+ const USAGE = '用法: discussion_service.mjs start|stop|restart|status|open|run|list|add|remove|clean|shortcut|autostart [--config 私有配置.json] [on|off]';
47
47
  export const CONNECTION_FIELDS = ['executable', 'ipc_directory'];
48
48
 
49
49
  /** Read one atomic configuration version for editing and worker acknowledgement. */
@@ -798,6 +798,7 @@ export async function main(args = process.argv.slice(2), print = value => proces
798
798
  case 'run': return run_service(home);
799
799
  case 'start': return print(await start_service(home));
800
800
  case 'stop': return print(await stop_service(home));
801
+ case 'restart': await stop_service(home); return print(await start_service(home));
801
802
  case 'status': return print(service_status(home));
802
803
  case 'open': return print({ ...(await start_service(home)), opened: open_sites(home) });
803
804
  case 'list': return print({ home, configs: read_registry(home) });
@@ -0,0 +1,78 @@
1
+ /** Read-only directory and text previews, confined to a room's saved project directory. */
2
+ import { closeSync, fstatSync, openSync, readdirSync, readSync, realpathSync, statSync } from 'node:fs';
3
+ import { isAbsolute, relative, resolve } from 'node:path';
4
+
5
+ const DIRECTORY_PAGE_SIZE = 200;
6
+ // Leave room for JSON escaping and base64 within the queued relay's 512 KiB encrypted envelope.
7
+ const PREVIEW_MAX_BYTES = 128 * 1024;
8
+ const DIRECTORY_MAX_BYTES = 128 * 1024;
9
+
10
+ /** Resolve a relative project path, including links, without allowing an escape from the project. */
11
+ function project_path(root, path) {
12
+ if (typeof path !== 'string' || /[\0:]/.test(path) || isAbsolute(path) || path.replaceAll('\\', '/').split('/').includes('..')) {
13
+ throw new Error('只能读取项目目录内的相对路径');
14
+ }
15
+ const target = realpathSync(resolve(root, path));
16
+ const inside = relative(root, target);
17
+ if (inside === '..' || inside.startsWith('../') || inside.startsWith('..\\') || isAbsolute(inside)) {
18
+ throw new Error('此链接指向项目目录之外,无法读取');
19
+ }
20
+ return target;
21
+ }
22
+
23
+ /** Describe one entry; broken or external links remain visible but cannot be opened. */
24
+ function directory_entry(root, path, entry) {
25
+ const child = path ? `${path}/${entry.name}` : entry.name;
26
+ try {
27
+ const info = statSync(project_path(root, child));
28
+ const kind = info.isDirectory() ? 'directory' : info.isFile() ? 'file' : 'unavailable';
29
+ return { name: entry.name, path: child, kind, size: info.isFile() ? info.size : null, modified_at: info.mtime.toISOString() };
30
+ } catch {
31
+ return { name: entry.name, path: child, kind: 'unavailable', size: null, modified_at: null };
32
+ }
33
+ }
34
+
35
+ /** Read a bounded UTF-8 or BOM-marked UTF-16 preview, returning a reason for binary files. */
36
+ function text_preview(target) {
37
+ const descriptor = openSync(target, 'r');
38
+ try {
39
+ const info = fstatSync(descriptor);
40
+ if (!info.isFile()) throw new Error('只能预览普通文件');
41
+ const bytes = Buffer.alloc(Math.min(info.size, PREVIEW_MAX_BYTES));
42
+ const length = readSync(descriptor, bytes, 0, bytes.length, 0);
43
+ const truncated = info.size > length;
44
+ const encoding = bytes[0] === 0xff && bytes[1] === 0xfe ? 'utf-16le'
45
+ : bytes[0] === 0xfe && bytes[1] === 0xff ? 'utf-16be' : 'utf-8';
46
+ const result = { size: info.size, modified_at: info.mtime.toISOString(), truncated, limit: PREVIEW_MAX_BYTES, encoding };
47
+ try {
48
+ const text = new TextDecoder(encoding, { fatal: true }).decode(bytes.subarray(0, length), { stream: truncated });
49
+ if (/[\u0000-\u0008\u000b\u000e-\u001f]/.test(text)) throw new Error('binary');
50
+ return { ...result, text };
51
+ } catch { return { ...result, text: null, reason: '此文件为二进制或使用了暂不支持的文本编码' }; }
52
+ } finally { closeSync(descriptor); }
53
+ }
54
+
55
+ /** List one directory page or preview one file under the configured project root. */
56
+ export function read_project_files(cwd, path = '', offset = 0) {
57
+ if (!cwd?.trim()) throw new Error('本房间未设置项目目录');
58
+ if (!Number.isSafeInteger(offset) || offset < 0) throw new Error('目录分页位置无效');
59
+ const root = realpathSync(cwd);
60
+ if (!statSync(root).isDirectory()) throw new Error('项目目录不是文件夹');
61
+ const target = project_path(root, path);
62
+ const info = statSync(target);
63
+ const normalized = path.replaceAll('\\', '/').split('/').filter(part => part && part !== '.').join('/');
64
+ if (info.isFile()) return { path: normalized, kind: 'file', ...text_preview(target) };
65
+ if (!info.isDirectory()) throw new Error('只能浏览文件夹或预览普通文件');
66
+ const items = readdirSync(target, { withFileTypes: true }).sort((left, right) =>
67
+ Number(right.isDirectory()) - Number(left.isDirectory()) || left.name.localeCompare(right.name, 'zh-CN', { numeric: true }));
68
+ const entries = [];
69
+ let bytes = 0;
70
+ for (const entry of items.slice(offset, offset + DIRECTORY_PAGE_SIZE)) {
71
+ const item = directory_entry(root, normalized, entry);
72
+ bytes += Buffer.byteLength(JSON.stringify(item));
73
+ if (entries.length && bytes > DIRECTORY_MAX_BYTES) break;
74
+ entries.push(item);
75
+ }
76
+ return { path: normalized, kind: 'directory', entries, total: items.length,
77
+ next_offset: offset + entries.length < items.length ? offset + entries.length : null };
78
+ }
@@ -27,9 +27,9 @@ const RELAY_CIPHER_INFO = 'codiscuss-relay-v1';
27
27
  const LAUNCH_SCRIPT = fileURLToPath(new URL('./discussion_launch.mjs', import.meta.url));
28
28
  // What a connected browser may ask the local bridge for, minus the folder dialog nobody would see and pairing itself.
29
29
  const RELAYED_ACTIONS = new Set(['settings', 'launch', 'room-models', 'room-member', 'room-member-access', 'room-member-allow', 'room-member-adapter',
30
- 'room-member-model', 'room-profiles', 'room-restore', 'room-fork', 'models', 'api-key', 'api-check', 'scheduler-check', 'tts-settings', 'tts', 'stt', 'projects', 'presets', 'admin-account']);
30
+ 'room-member-model', 'room-profiles', 'room-files', 'room-restore', 'room-fork', 'models', 'api-key', 'api-check', 'scheduler-check', 'tts-settings', 'tts', 'stt', 'projects', 'presets', 'admin-account']);
31
31
  export const ROOM_ACTIONS = new Set(['launch', 'room-models', 'room-member', 'room-member-access', 'room-member-allow', 'room-member-adapter',
32
- 'room-member-model', 'room-profiles', 'room-restore', 'room-fork']);
32
+ 'room-member-model', 'room-profiles', 'room-files', 'room-restore', 'room-fork']);
33
33
 
34
34
  /** Read every site this computer is paired with. */
35
35
  export function machine_links(home) {
package/runner/stt.mjs CHANGED
@@ -10,7 +10,9 @@ import { read_saved_api_key } from './discussion_api.mjs';
10
10
  import { TTS_BASE_URL, tts_settings } from './tts.mjs';
11
11
 
12
12
  const STT_ENDPOINT = `${TTS_BASE_URL}/api/v3/auc/bigmodel/recognize/flash`;
13
- const STT_RESOURCE_ID = 'volc.bigasr.auc_turbo';
13
+ // The console's “录音文件识别2.0” grants this resource; it answers on the flash route (verified 2026-11-03). The 1.0
14
+ // flash resource `volc.bigasr.auc_turbo` is a separate product and is refused with 45000030 when only 2.0 is enabled.
15
+ const STT_RESOURCE_ID = 'volc.seedasr.auc';
14
16
  const STT_DONE_CODE = '20000000';
15
17
  const STT_SILENT_CODE = '20000003';
16
18
  const STT_TIMEOUT_MS = 50_000;
@@ -22,6 +24,7 @@ const STALE_MS = 60 * 60 * 1000;
22
24
  const STATUS_MESSAGES = {
23
25
  45000001: '请求参数有误',
24
26
  45000002: '没有录到声音',
27
+ 45000030: '火山引擎未授权该识别服务:请在控制台开通「录音文件识别2.0」',
25
28
  45000151: '音频格式不受支持',
26
29
  55000031: '火山引擎语音识别繁忙,请稍后再试',
27
30
  };
@@ -54,8 +57,8 @@ async function recognize(key, wav, request) {
54
57
  'X-Api-Request-Id': randomUUID(), 'X-Api-Sequence': '-1' },
55
58
  body: JSON.stringify({ user: { uid: 'codiscuss' }, audio: { data: wav.toString('base64') }, request: { model_name: 'bigmodel', enable_punc: true, enable_itn: true } }) });
56
59
  } catch { throw new Error('语音识别请求超时或连接失败,请检查网络后重试'); }
57
- if (response.status === 401 || response.status === 403) throw new Error('火山引擎拒绝了请求:API Key 无效或未开通录音文件识别 2.0');
58
60
  const code = response.headers.get('X-Api-Status-Code');
61
+ if ((response.status === 401 || response.status === 403) && !STATUS_MESSAGES[code]) throw new Error('火山引擎拒绝了请求:API Key 无效或未开通录音文件识别 2.0');
59
62
  if (code === STT_SILENT_CODE) return '';
60
63
  if (code !== STT_DONE_CODE) {
61
64
  const message = STATUS_MESSAGES[code] || response.headers.get('X-Api-Message') || `HTTP ${response.status}`;
package/web/app.js CHANGED
@@ -2453,9 +2453,9 @@ async function retry_launch_connection() {
2453
2453
  /** Read saved choices for the active room without using drafts or delaying discussion refreshes. */
2454
2454
  async function load_room_models() {
2455
2455
  const room = state.room;
2456
- // Only members this computer wakes make the models worth reading.
2456
+ // The saved project directory also applies to rooms containing only manual members.
2457
2457
  const woken = (room?.members || []).filter(member => member.access === "runner").map(member => member.name);
2458
- if (!state.launch.available || !woken.length) return;
2458
+ if (!state.launch.available || !room) return;
2459
2459
  // The woken names are part of the key; each new execution may run another model, which only its session log tells, so the latest executions are too.
2460
2460
  const executions = (room.free?.members || []).map(member => member.execution?.id || 0).join(",");
2461
2461
  const revision = `${woken.join(",")}:${executions}`;
@@ -2463,13 +2463,15 @@ async function load_room_models() {
2463
2463
  if (previous?.room_id === room.id && previous.revision === revision
2464
2464
  && (previous.loading || previous.participants)) return;
2465
2465
  // The last answer stays on screen while the new one is read.
2466
- const current = { room_id: room.id, revision, loading: true, participants: previous?.room_id === room.id ? previous.participants : null };
2466
+ const current = { room_id: room.id, revision, loading: true, participants: previous?.room_id === room.id ? previous.participants : null,
2467
+ project_directory: previous?.room_id === room.id ? previous.project_directory : "" };
2467
2468
  state.launch.room_models = current;
2468
2469
  const live = () => state.launch.room_models === current && state.room?.id === room.id;
2469
2470
  // agy is run to name its default model, which takes seconds; everything else is shown first and agy follows.
2470
2471
  try {
2471
2472
  const result = await local_launch_request("room-models", { room_id: room.id, quick: true });
2472
- if (live() && result.room_id === room.id) Object.assign(current, { participants: result.participants, registered: result.registered, scheduler: result.scheduler || null });
2473
+ if (live() && result.room_id === room.id) Object.assign(current, { participants: result.participants, registered: result.registered,
2474
+ scheduler: result.scheduler || null, project_directory: result.project_directory || "" });
2473
2475
  } catch (error) {
2474
2476
  // Unavailable or older bridges leave the model unknown; the failure is kept so the page can say so, and the next refresh retries.
2475
2477
  Object.assign(current, { participants: null, error: error.message || "读取失败" });
@@ -2494,6 +2496,11 @@ function reload_room_models() {
2494
2496
  return load_room_models();
2495
2497
  }
2496
2498
 
2499
+ /** Read a directory or file from the room's saved project through the authenticated local connection. */
2500
+ function read_room_files(room_id, path = "", offset = 0) {
2501
+ return local_launch_request("room-files", { room_id, path, offset, token: localStorage.getItem(`discuss-access:${room_id}`) });
2502
+ }
2503
+
2497
2504
  /**
2498
2505
  * Describe registered launch choices: the card's choice, else what the member's session last ran with, else what the software's settings select;
2499
2506
  * anything unknown is "默认".
@@ -4168,7 +4175,7 @@ window.discuss_view = { snapshot: view_snapshot, write_field: write_view_field,
4168
4175
  admin: { register: register_admin, login: login_admin, logout: logout_admin,
4169
4176
  update_profile: update_admin_profile, avatar_from_file: admin_avatar_from_file, validate: validate_admin_form },
4170
4177
  request: request_json, rewind_plan, identities: IDENTITIES, display_name, mention_handles, mentioned_members, sources: SOURCE_LABELS,
4171
- set_member_access, set_member_model, allow_denials, set_member_adapter, reload_room_models, limits_label, stop_reason_label, participant_model, participant_files, participant_session_hint, participant_access,
4178
+ set_member_access, set_member_model, allow_denials, set_member_adapter, reload_room_models, read_room_files, limits_label, stop_reason_label, participant_model, participant_files, participant_session_hint, participant_access,
4172
4179
  turn_label, turn_guidance, round_groups, limit_rounds, progress_speaker, brief_meta, max_message_length: MAX_MESSAGE_LENGTH, provenance: message_provenance, format_time };
4173
4180
 
4174
4181
  restore_theme();