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
|
@@ -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: [
|
|
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);
|