ai_co_discuss 0.1.9 → 0.1.10

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/docs/runner.md CHANGED
@@ -38,6 +38,10 @@ codiscuss local stop
38
38
 
39
39
  网站数据库为服务目录下 `local/discuss.sqlite3`,配置、登记表和讨论执行数据也位于包外;服务目录位置及 `DISCUSS_SERVICE_HOME` 见下文。源码运行的 `data/discuss.sqlite3` 不自动迁移。网站仍在而讨论服务已停止时,显式 `local open/start` 会要求网站管理进程恢复服务;不会自动恢复已暂停的房间。`local status` 的 `running` 要求网站和讨论服务同时运行,`website_running` 单独表示网站状态。`local stop` 关闭它创建的网站,并停止它启动或恢复且仍由相同 PID 持有的讨论服务;预先运行或后来独立启动的服务会保留。停止服务会暂停其托管中的活动讨论,数据库保留。日志在 `local/website.log`、`local/supervisor.log` 与原 `service.log`。
40
40
 
41
+ Windows 的 MSIX 应用(如 Claude 桌面版)可能把派生进程对 AppData 的写入重定向到 `Packages/<包名>/LocalCache/Local/`;同名数据库、锁和停止请求因此与普通终端分离。管理命令在读状态、建房或启动进程前,用临时文件核实实际写入位置,发现重定向就报出两处路径并停止,避免误报已启动或已停止;帮助和纯 HTTP 网站命令仍可使用。检测依据是文件实际位置,不依赖进程是否报告包身份。Python 启动别名与直接执行原生 Python 也可能进入不同文件视图,排查时须核对网站实际使用的可执行文件与数据库。此机制见 [Microsoft MSIX 文件虚拟化说明](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-behind-the-scenes)。
42
+
43
+ 遇到该错误,从应用外的普通终端管理服务;需要所有入口直接管理时,为它们统一设置 AppData 之外的 `DISCUSS_SERVICE_HOME`,已有数据需停机备份后迁移,不会自动合并两份库。若旧版本已在包私有目录启动服务,先核对该目录的 `local/status.json`、`status.json` 和实际 PID,再在普通终端将 `DISCUSS_SERVICE_HOME` 显式设为该物理目录执行 `local stop`;独立启动的讨论服务仍需单独 `service stop`,`local stop` 会明确报告保留的 PID。停止请求必须与目标进程使用同一份目录,仅看到另一个目录没有锁不能证明目标已退出。
44
+
41
45
  已安装后升级不要使用 `npx`(它会运行已安装的旧版本):先 `codiscuss local stop`,再 `npm install --global --prefer-online ai_co_discuss@latest`,然后重跑 `codiscuss setup`;`--prefer-online` 避免本地缓存把 `latest` 解析为旧版本。
42
46
 
43
47
  卸载先 `codiscuss local stop`,按需要另用 `codiscuss service stop` 停止预先独立运行的服务,再执行 `npm uninstall --global ai_co_discuss`;自定义 prefix 时带回相同 `--prefix`。安装器不删除数据库、私有配置或已复制技能。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai_co_discuss",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "bin": {
5
5
  "codiscuss": "runner/codiscuss.mjs"
6
6
  },
@@ -8,7 +8,7 @@ import { parseArgs } from 'node:util';
8
8
 
9
9
  import { atomic_json } from './discussion_adapters.mjs';
10
10
  import { acquire_lock, api, DiscussionStore, service_drafts, task_files, validate_config } from './discussion_runner.mjs';
11
- import { CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
11
+ import { assert_shared_home, CONNECTION_FIELDS, configuration_snapshot, read_registry, registered_identity, register_room, service_home, service_pid, start_service } from './discussion_service.mjs';
12
12
 
13
13
  const CLIENTS = { Codex: ['codex_cli', 'codex'], Claude: ['claude'], Gemini: ['antigravity_cli', 'antigravity'] };
14
14
  // Reasoning effort is a separate CLI argument only for Codex and Claude; agy encodes it in the model name.
@@ -339,6 +339,7 @@ async function prepare_launch(home, input, directory, options = {}) {
339
339
 
340
340
  /** Launch a room through either entry, using a durable journal for safe retries. */
341
341
  export async function launch_discussion(home, raw, options = {}) {
342
+ assert_shared_home(home);
342
343
  const input = launch_input(raw);
343
344
  const key = createHash('sha256').update(`${input.site}/${input.room_id || input.room.submission_id}`).digest('hex');
344
345
  const directory = join(home, 'launches', key);
@@ -401,6 +402,7 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
401
402
  if (values.help || !positionals.length) return process.stdout.write(HELP);
402
403
  const input = input_text === undefined ? (values.file ? JSON.parse(readFileSync(values.file, 'utf8').replace(/^\uFEFF/, '')) : null) : JSON.parse(input_text);
403
404
  const home = service_home();
405
+ assert_shared_home(home);
404
406
  let result;
405
407
  if (positionals[0] === 'settings') result = input ? save_launch_settings(home, input) : launch_settings(home);
406
408
  else if (positionals[0] === 'start' && input) result = await launch_discussion(home, input, { dry_run: values['dry-run'], browser_request: values.browser });
@@ -9,7 +9,7 @@ import { parseArgs } from 'node:util';
9
9
 
10
10
  import { atomic_json } from './discussion_adapters.mjs';
11
11
  import { acquire_lock } from './discussion_runner.mjs';
12
- import { main as service_main, service_home, service_pid, service_ready, start_service } from './discussion_service.mjs';
12
+ import { assert_shared_home, main as service_main, service_home, service_pid, service_ready, start_service } from './discussion_service.mjs';
13
13
  import { detect_python, local_settings } from './discussion_setup.mjs';
14
14
  import { tone } from './discussion_tui.mjs';
15
15
 
@@ -142,6 +142,7 @@ async function requested_service(home, status, owned_service) {
142
142
 
143
143
  /** Run one supervisor, recording readiness and handling an explicit local stop request. */
144
144
  async function supervise(home) {
145
+ assert_shared_home(home);
145
146
  const directory = join(home, 'local');
146
147
  const release = acquire_lock(directory);
147
148
  const status_path = join(directory, 'status.json');
@@ -239,7 +240,8 @@ function local_summary(command, result, opened) {
239
240
  status: result.running ? [`本机网站运行中:${site}`]
240
241
  : result.website_running ? [`本机网站运行中:${site},但讨论服务未运行;运行 codiscuss local open 恢复`]
241
242
  : ['本机网站未运行;运行 codiscuss local open 启动'],
242
- stop: ['本机网站与讨论服务已停止;数据已保留,运行 codiscuss local open 重新启动'],
243
+ stop: result.service_pid ? [`本机网站已停止;独立讨论服务仍在运行(PID ${result.service_pid}),可用 codiscuss service stop 单独停止;数据已保留`]
244
+ : ['本机网站与讨论服务已停止;数据已保留,运行 codiscuss local open 重新启动'],
243
245
  connect: [`已连接 ${result.connected};本机网站 ${tone.cyan(result.local_url || '')}`,
244
246
  opened ? '已在浏览器打开连接链接,请在该页面完成接入;连接链接不要转发' : `请在浏览器打开连接链接完成接入,链接不要转发:${result.url}`],
245
247
  disconnect: [`已撤销 ${result.disconnected} 的本机启动权限;正在运行的讨论需在进度看板暂停`],
@@ -258,6 +260,7 @@ export async function local_main(args, output) {
258
260
  const [command = 'open', site] = positionals;
259
261
  const home = service_home();
260
262
  if (values.help || command === 'help') return process.stdout.write(LOCAL_HELP);
263
+ assert_shared_home(home);
261
264
  const report = (result, opened) => {
262
265
  if (output) return output(result);
263
266
  local_summary(command, result, opened);
@@ -1,7 +1,7 @@
1
1
  /** Resident local service that keeps every registered automatic room consumed; no model API orchestration. */
2
2
  import { spawn, spawnSync } from 'node:child_process';
3
- import { createHash } from 'node:crypto';
4
- import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync } from 'node:fs';
3
+ import { createHash, randomUUID } from 'node:crypto';
4
+ import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, unlinkSync } from 'node:fs';
5
5
  import { homedir } from 'node:os';
6
6
  import { dirname, join, resolve } from 'node:path';
7
7
  import { DatabaseSync } from 'node:sqlite';
@@ -35,6 +35,29 @@ export function service_home() {
35
35
  return join(process.env.LOCALAPPDATA || join(homedir(), '.local', 'state'), 'ai-codiscuss', 'service');
36
36
  }
37
37
 
38
+ /** Reject an MSIX-private write view before reading shared service state or changing it. */
39
+ export function assert_shared_home(home) {
40
+ if (process.platform !== 'win32') return;
41
+ let directory = resolve(home);
42
+ while (!existsSync(directory)) {
43
+ const parent = dirname(directory);
44
+ if (parent === directory) throw new Error(`服务目录没有可访问的父目录:${home}`);
45
+ directory = parent;
46
+ }
47
+ const probe = join(directory, `.codiscuss-path-${randomUUID()}`);
48
+ closeSync(openSync(probe, 'wx', 0o600));
49
+ try {
50
+ const actual = realpathSync.native(probe);
51
+ // Compare the container suffix so a short user-profile name (ADMINI~1) is not a redirect.
52
+ const container = actual.match(/\\AppData\\Local\\Packages\\[^\\]+\\LocalCache\\(?:Local|Roaming)\\/i)?.[0];
53
+ if (container && !probe.toLowerCase().includes(container.toLowerCase())) {
54
+ throw new Error(`Windows MSIX 正在重定向服务目录 ${home},实际写入 ${dirname(actual)}。`
55
+ + '请从应用外的普通终端执行 codiscuss local start/stop;'
56
+ + '或让所有入口使用同一个 AppData 之外的 DISCUSS_SERVICE_HOME。原数据不会自动迁移。');
57
+ }
58
+ } finally { unlinkSync(probe); }
59
+ }
60
+
38
61
  /** Read the registered private room configuration paths. */
39
62
  export function read_registry(home) {
40
63
  const path = join(home, 'rooms.json');
@@ -203,6 +226,7 @@ export function service_ready(home, pid = service_pid(home)) {
203
226
 
204
227
  /** Foreground service loop; `start` runs this detached in the background. */
205
228
  export async function run_service(home, output = () => {}) {
229
+ assert_shared_home(home);
206
230
  const release = acquire_lock(home);
207
231
  const log_line = message => {
208
232
  const line = `${new Date().toISOString()} ${message}`;
@@ -236,6 +260,7 @@ export async function run_service(home, output = () => {}) {
236
260
 
237
261
  /** Start the detached service once; an already running service is reused. */
238
262
  export async function start_service(home) {
263
+ assert_shared_home(home);
239
264
  const running = service_pid(home);
240
265
  if (service_ready(home, running)) return { running: true, ready: true, pid: running, started: false };
241
266
  mkdirSync(home, { recursive: true });
@@ -329,6 +354,7 @@ export async function main(args = process.argv.slice(2), print = value => proces
329
354
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { config: { type: 'string' } } });
330
355
  const [command, option] = positionals;
331
356
  const home = service_home();
357
+ assert_shared_home(home);
332
358
  const require_config = () => { if (!values.config) throw new Error('请用 --config 指定私有房间配置'); return values.config; };
333
359
  switch (command) {
334
360
  case 'run': return run_service(home);
@@ -8,7 +8,7 @@ import { parseArgs } from 'node:util';
8
8
 
9
9
  import { atomic_json } from './discussion_adapters.mjs';
10
10
  import { launch_settings, save_launch_settings } from './discussion_launch.mjs';
11
- import { service_home } from './discussion_service.mjs';
11
+ import { assert_shared_home, service_home } from './discussion_service.mjs';
12
12
  import { install_skills } from './discussion_skills.mjs';
13
13
  import { ask, checkbox_prompt, intro, outro, pad, short_path, spinner, step, styled, text_width, tone } from './discussion_tui.mjs';
14
14
  export { checkbox_prompt } from './discussion_tui.mjs';
@@ -58,6 +58,7 @@ export function local_settings(home = service_home()) {
58
58
  function setup_environment(options) {
59
59
  if (Number(process.versions.node.split('.')[0]) < MIN_NODE_MAJOR) throw new Error('需要 Node.js 24+,请升级后重试。');
60
60
  const home = service_home();
61
+ assert_shared_home(home);
61
62
  const previous = local_settings(home);
62
63
  const python = detect_python(options.python || previous.python);
63
64
  const port = Number(options.port || previous.port || DEFAULT_PORT);