ai_co_discuss 0.3.46 → 0.3.49
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/.agents/skills/discuss-host/SKILL.md +1 -0
- package/.agents/skills/discuss-host/references/discussion.md +113 -113
- package/.agents/skills/discuss-host/references/free.md +36 -34
- package/.agents/skills/discuss-host/references/goal.md +35 -0
- package/.agents/skills/discuss-participant/SKILL.md +1 -0
- package/.agents/skills/workflow-creator/SKILL.md +2 -2
- package/README.md +3 -1
- package/backend/admin.py +104 -35
- package/backend/free_room.py +2170 -2136
- package/backend/goal_room.py +358 -0
- package/backend/mode_room.py +105 -0
- package/backend/schema.sql +1593 -1510
- package/backend/server.py +2636 -2555
- package/backend/session_room.py +1180 -1148
- package/docs/THIRD_PARTY_NOTICES.md +1293 -1264
- package/docs/deployment.md +5 -1
- package/docs/en/workflow.md +15 -1
- package/docs/runner.md +28 -7
- package/docs/workflow.md +15 -1
- package/package.json +3 -2
- package/runner/adapters/api.mjs +333 -313
- package/runner/adapters/claude.mjs +12 -16
- package/runner/admin_account.mjs +12 -19
- package/runner/device_auth.mjs +369 -0
- package/runner/discuss.py +1208 -1125
- package/runner/discussion_launch.mjs +107 -12
- package/runner/discussion_service.mjs +25 -1
- package/runner/expert_prompts.mjs +132 -132
- package/runner/free_runner.mjs +982 -910
- package/runner/pinyin_table.mjs +406 -0
- package/runner/project_files.mjs +52 -3
- package/runner/relay_channel.mjs +51 -7
- package/runner/remote_launch.mjs +221 -235
- package/runner/remote_policy.mjs +156 -0
- package/runner/scheduler.mjs +459 -429
- package/runner/stt.mjs +2 -2
- package/runner/tts.mjs +74 -113
- package/runner/tts_providers.mjs +205 -0
- package/runner/webauthn.mjs +155 -0
- package/runner/workflows.mjs +371 -348
- package/web/app.js +355 -51
- package/web/modern.css +1 -1
- package/web/modern.js +5764 -3816
|
@@ -11,6 +11,8 @@ import { parseArgs } from 'node:util';
|
|
|
11
11
|
|
|
12
12
|
import { atomic_json } from './files.mjs';
|
|
13
13
|
import { read_project_files } from './project_files.mjs';
|
|
14
|
+
import { guard_remote_request, list_remote_directories } from './remote_policy.mjs';
|
|
15
|
+
import { list_devices, revoke_device, revoke_write, start_pairing, write_allowed, write_needed } from './device_auth.mjs';
|
|
14
16
|
import { setup_sidecar } from './adapters/antigravity_app.mjs';
|
|
15
17
|
import { adapter_of, adoption_adapter, BRANCHABLE_ADAPTERS, branch_session, session_settings, software_defaults, software_effort, software_models } from './adapters/index.mjs';
|
|
16
18
|
import { ADAPTER_IDS, ADAPTER_PROFILES, adapter_profile, platform_adapters, same_session_family } from './adapters/profiles.mjs';
|
|
@@ -73,10 +75,12 @@ const HELP = `用法:codiscuss launch settings [--file FILE]
|
|
|
73
75
|
codiscuss launch room-restore --file FILE
|
|
74
76
|
codiscuss launch room-fork --file FILE
|
|
75
77
|
codiscuss launch pick-directory
|
|
78
|
+
codiscuss launch browse-directory [--file FILE]
|
|
79
|
+
codiscuss launch device-pairing [--file FILE]
|
|
80
|
+
codiscuss launch devices [--file FILE]
|
|
76
81
|
codiscuss launch projects [--file FILE]
|
|
77
82
|
codiscuss launch presets [--file FILE]
|
|
78
83
|
codiscuss launch machine-link --file FILE
|
|
79
|
-
codiscuss launch admin-account --file FILE
|
|
80
84
|
codiscuss launch workflow-adopt --draft FILE [--access-file FILE]
|
|
81
85
|
codiscuss launch workflow-check (--dir DIR | --draft FILE)
|
|
82
86
|
codiscuss launch workflow-save (--dir DIR | --draft FILE)
|
|
@@ -90,14 +94,16 @@ models 按 adapter、executable 或 API profile 读取模型目录,不启动
|
|
|
90
94
|
api-key 将 base_url、api_key 保存于本机私有目录,只返回 api_key_ref。
|
|
91
95
|
api-check 用 API profile 向模型发送一次极短请求,核对地址、密钥、协议与模型是否可用。
|
|
92
96
|
scheduler-check 用智能调度配置(provider、model、api_key_ref)向决策模型发送一道是非题,核对供应商、模型与密钥是否可用。
|
|
93
|
-
tts-settings
|
|
97
|
+
tts-settings 读取朗读设置;内容非空时保存(语音服务 provider:volcengine / aliyun / azure,语速 speech_rate、语气提示 context,以及 providers.<服务> 的 api_key_ref、speaker、member_voices、file_voice,Azure 另有 region)。
|
|
94
98
|
tts 用朗读设置把 text(不超过 300 字)合成为 MP3,返回 base64。
|
|
95
99
|
stt 按序接收录音片段(upload_id、index、base64 PCM 16 kHz 单声道、final);最后一段时识别整段录音并返回 text。
|
|
96
100
|
pick-directory 在本机弹出系统目录选择窗口,返回所选路径;取消时不返回路径。
|
|
101
|
+
browse-directory 列出常用项目及其中的子文件夹(只含文件夹名,分页),供其他设备选择目录;path 为空时列出常用项目。
|
|
102
|
+
device-pairing 在本机生成 12 位设备配对码(2 分钟有效,可选 temporary 临时设备、site 生成扫码链接);只能在这台电脑上执行。
|
|
103
|
+
devices 列出已配对设备;提供 revoke(设备 id)撤销设备,或 revoke_write(device_id、room_id)撤销远程可写授权。
|
|
97
104
|
projects 读取本机已配置的项目(名称与绝对路径);提供 JSON(projects 列表)时整体替换保存。
|
|
98
105
|
presets 读取本机的专家预设(含未删除的内置预设)与各平台默认描述;提供 JSON(presets 列表、description_templates 和/或 hidden_builtins 已删除的内置预设名)时替换对应部分。
|
|
99
106
|
machine-link 返回本机连接指定 site 的电脑编号、名称与凭据,供已登录管理员的网页完成配对;不存在时创建。
|
|
100
|
-
admin-account 把在指定 site 注册的管理员用户名与密码保存到服务目录的 admin-accounts.json;须提供 site、username、password。
|
|
101
107
|
room-member 为房间邀请、请回一位成员;须提供 site、room_id、token、action(add/rejoin)、name。
|
|
102
108
|
room-member-access 修改一位本机唤醒成员的项目权限(project_access 为 read/write/auto/full),下次投递起生效、保留原会话;须提供 site、room_id、token、name、project_access。
|
|
103
109
|
room-member-allow 把被拒绝的操作加入一位本机唤醒的 Claude Code 或 agy 成员的 allowed_tools,下次投递起生效;须提供 site、room_id、token、name、rules。
|
|
@@ -550,6 +556,13 @@ function free_launch_config(home, input, room, directory, options) {
|
|
|
550
556
|
host_workflows: input.host_workflows === true || undefined, data_directory: join(directory, 'runtime'), draft_directory: service_drafts(home, join(directory, 'runtime')) }, directory);
|
|
551
557
|
}
|
|
552
558
|
|
|
559
|
+
/** Members in the order of the creation form's cards, so the room's joining order (and default speaking order) follows it. */
|
|
560
|
+
function card_order(cards, members) {
|
|
561
|
+
const names = (cards || []).map(member => typeof member === 'string' ? member : member?.name);
|
|
562
|
+
const place = (name) => names.includes(name) ? names.indexOf(name) : names.length;
|
|
563
|
+
return [...members].sort((left, right) => place(left.name) - place(right.name));
|
|
564
|
+
}
|
|
565
|
+
|
|
553
566
|
/** Preserve caller-generated member credentials; journal newly generated ones before creation. */
|
|
554
567
|
function launch_members(input, names, options) {
|
|
555
568
|
return Object.fromEntries(names.map(name => {
|
|
@@ -558,12 +571,17 @@ function launch_members(input, names, options) {
|
|
|
558
571
|
}));
|
|
559
572
|
}
|
|
560
573
|
|
|
561
|
-
/**
|
|
562
|
-
|
|
574
|
+
/**
|
|
575
|
+
* Merge one participant's machine defaults with this launch's overrides and check its software. A launch from another
|
|
576
|
+
* device (`input.without_saved_tools`) takes no tool approvals saved here, whichever way its members are named.
|
|
577
|
+
*/
|
|
578
|
+
export function participant_profile(name, defaults, input, current_host) {
|
|
563
579
|
const platform = platform_of(name);
|
|
564
580
|
// An extra instance starts from its platform's settings but never from another instance's bound session.
|
|
565
581
|
const { session_id: ignored_session, ...inherited } = name === platform ? {} : input.participants?.[platform] || {};
|
|
566
|
-
const
|
|
582
|
+
const saved = { ...defaults[platform], ...(name === platform ? {} : defaults[name]) };
|
|
583
|
+
if (input.without_saved_tools) delete saved.allowed_tools;
|
|
584
|
+
const profile = { ...saved, ...inherited, ...input.participants?.[name] };
|
|
567
585
|
for (const field of ['cwd', 'model', 'effort', 'project_name', 'project_id', 'project_access']) if (profile[field] === '') delete profile[field];
|
|
568
586
|
validate_profile(name, profile, true);
|
|
569
587
|
if (!clients_of(name)?.includes(profile.adapter)) throw new Error(`请配置 ${name} 的 CLI / App 客户端`);
|
|
@@ -1293,11 +1311,11 @@ async function perform_launch(home, context, options) {
|
|
|
1293
1311
|
let room = context.room;
|
|
1294
1312
|
if (!input.room_id) room = await site_request(input.site, '/api/rooms', config.token, {
|
|
1295
1313
|
...input.room,
|
|
1296
|
-
members: [...Object.keys(config.participants).map(name => ({ name, adapter: config.participants[name].adapter,
|
|
1314
|
+
members: card_order(input.room.members, [...Object.keys(config.participants).map(name => ({ name, adapter: config.participants[name].adapter,
|
|
1297
1315
|
token: config.members[name].token, access: 'runner', session_mode: config.participants[name].session_id ? 'borrowed' : 'new' })),
|
|
1298
1316
|
// Manual members keep the credentials the browser generated; nothing local is configured for them.
|
|
1299
1317
|
...(input.room.members || []).filter(member => record.discussion ? member?.access === 'manual' : !woken_member(member))
|
|
1300
|
-
.map(({ name, token, access }) => ({ name, token, access, adapter: '', session_mode: 'new' }))],
|
|
1318
|
+
.map(({ name, token, access }) => ({ name, token, access, adapter: '', session_mode: 'new' }))]),
|
|
1301
1319
|
});
|
|
1302
1320
|
config.room = room.id;
|
|
1303
1321
|
await verify_control(config, room);
|
|
@@ -1419,7 +1437,7 @@ export async function adopt_workflow(home, { access_file, draft_file }) {
|
|
|
1419
1437
|
const path = registered_room_path(home, String(access.site || '').replace(/\/$/, ''), access.room);
|
|
1420
1438
|
if (!path) throw new Error('本机没有登记这个房间');
|
|
1421
1439
|
const config = configuration_snapshot(path).config;
|
|
1422
|
-
if (!config.host_workflows) throw new Error('
|
|
1440
|
+
if (!config.host_workflows) throw new Error('房主没有允许本房间的主持人自建工作流;请让用户在“成员与设置 → 协作设置”打开后再保存');
|
|
1423
1441
|
const host = Object.keys(config.members || {}).find(name => config.members[name].token === access.token);
|
|
1424
1442
|
const room = await api(config, '');
|
|
1425
1443
|
const roster = room.free?.members || [];
|
|
@@ -1466,10 +1484,64 @@ export function check_or_save_workflow(home, { dir, draft }, save = false) {
|
|
|
1466
1484
|
return save_workflow(home, { ...preview, agents }, validate_preset);
|
|
1467
1485
|
}
|
|
1468
1486
|
|
|
1487
|
+
const WRITING_ACCESS = new Set(['write', 'auto', 'full', 'own']);
|
|
1488
|
+
|
|
1489
|
+
/** The configuration this computer keeps for a room, found by its id; null when it runs no such room. */
|
|
1490
|
+
function room_config_by_id(home, room_id) {
|
|
1491
|
+
if (typeof room_id !== 'string' || !room_id) return null;
|
|
1492
|
+
const paths = read_registry(home).filter(path => registered_identity(path)?.room === room_id);
|
|
1493
|
+
return paths.length === 1 ? configuration_snapshot(paths[0]).config : null;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/** Whether a member may change files here: its own level, else the room's, or any tool approved for it. */
|
|
1497
|
+
const member_writes = (config, profile) => WRITING_ACCESS.has(profile?.project_access || config?.project_access) || profile?.allowed_tools?.length > 0;
|
|
1498
|
+
|
|
1499
|
+
/**
|
|
1500
|
+
* The room in which a relayed command would put a member with write access to work, judged from what this computer
|
|
1501
|
+
* keeps rather than from the request text; null when it would not. Such a command needs the device's write grant.
|
|
1502
|
+
*/
|
|
1503
|
+
export function remote_write_room(home, command, input) {
|
|
1504
|
+
if (!input || typeof input !== 'object') return null;
|
|
1505
|
+
// Approving a tool widens what a member can do, whatever its level: it always needs the grant.
|
|
1506
|
+
if (command === 'room-member-allow') return input.room_id || null;
|
|
1507
|
+
if (command === 'room-member') {
|
|
1508
|
+
const config = room_config_by_id(home, input.room_id);
|
|
1509
|
+
return config && member_writes(config, config.participants?.[input.name]) ? input.room_id : null;
|
|
1510
|
+
}
|
|
1511
|
+
if (command === 'room-fork') {
|
|
1512
|
+
const config = room_config_by_id(home, input.source_room_id);
|
|
1513
|
+
return config && Object.values(config.participants || {}).some(profile => member_writes(config, profile)) ? input.source_room_id : null;
|
|
1514
|
+
}
|
|
1515
|
+
if (command === 'room-workflow') {
|
|
1516
|
+
if (input.action === 'host') throw new Error('“允许主持人自建工作流”只能在电脑上开启');
|
|
1517
|
+
if (input.action === 'enable') {
|
|
1518
|
+
const cards = JSON.stringify(get_workflow(home, input.workflow?.id));
|
|
1519
|
+
return /"(project_access|requested_access)":"(write|auto|full)"/.test(cards) ? input.room_id : null;
|
|
1520
|
+
}
|
|
1521
|
+
if (input.action === 'back') {
|
|
1522
|
+
const config = room_config_by_id(home, input.room_id);
|
|
1523
|
+
return config && Object.values(config.participants || {}).some(profile => member_writes(config, profile)) ? input.room_id : null;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (command === 'start' && input.workflow?.id && /"(project_access|requested_access)":"(write|auto|full)"/.test(JSON.stringify(get_workflow(home, input.workflow.id)))) {
|
|
1527
|
+
throw new Error('其他设备新建房间时成员须为只读;建好后在房间里开启可写(需通行密钥确认)');
|
|
1528
|
+
}
|
|
1529
|
+
return null;
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/** Fill program locations another device left out with the ones saved here, so saving defaults cannot drop them. */
|
|
1533
|
+
function keep_saved_locations(home, participants) {
|
|
1534
|
+
const saved = launch_settings(home).participants;
|
|
1535
|
+
return Object.fromEntries(Object.entries(participants).map(([name, profile]) => {
|
|
1536
|
+
const kept = Object.fromEntries(['executable', 'ipc_directory'].filter(field => !profile?.[field] && saved[name]?.[field]).map(field => [field, saved[name][field]]));
|
|
1537
|
+
return [name, { ...profile, ...kept }];
|
|
1538
|
+
}));
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1469
1541
|
/** CLI and local HTTP bridge use the same commands and configuration validation. */
|
|
1470
1542
|
export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
1471
1543
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
|
|
1472
|
-
file: { type: 'string' }, 'dry-run': { type: 'boolean' }, browser: { type: 'boolean' },
|
|
1544
|
+
file: { type: 'string' }, 'dry-run': { type: 'boolean' }, browser: { type: 'boolean' }, remote: { type: 'boolean' }, device: { type: 'string' },
|
|
1473
1545
|
'configuration-locked': { type: 'boolean' }, help: { type: 'boolean', short: 'h' },
|
|
1474
1546
|
draft: { type: 'string' }, 'access-file': { type: 'string' }, dir: { type: 'string' },
|
|
1475
1547
|
} });
|
|
@@ -1486,6 +1558,18 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1486
1558
|
const input = input_text === undefined ? (values.file ? JSON.parse(readFileSync(values.file, 'utf8').replace(/^\uFEFF/, '')) : null) : JSON.parse(input_text);
|
|
1487
1559
|
const home = service_home();
|
|
1488
1560
|
assert_shared_home(home);
|
|
1561
|
+
// Another device's request may only use what this computer granted; the flag is set by the relay, never by the request.
|
|
1562
|
+
if (values.remote) {
|
|
1563
|
+
const allowed = room => Boolean(values.device) && write_allowed(home, values.device, room);
|
|
1564
|
+
guard_remote_request(positionals[0], input, { projects: launch_projects(home).projects, participants: launch_settings(home).participants, home,
|
|
1565
|
+
write_allowed: allowed, write_needed });
|
|
1566
|
+
const room = remote_write_room(home, positionals[0], input);
|
|
1567
|
+
if (room && !allowed(room)) throw write_needed(room);
|
|
1568
|
+
// Another device repeats the saved program locations it cannot see; keep them rather than dropping them.
|
|
1569
|
+
if (positionals[0] === 'settings' && input?.participants) input.participants = keep_saved_locations(home, input.participants);
|
|
1570
|
+
// A room created from another device starts read-only, so its members do not take the tool approvals saved here.
|
|
1571
|
+
if (positionals[0] === 'start' && input) input.without_saved_tools = true;
|
|
1572
|
+
}
|
|
1489
1573
|
let result;
|
|
1490
1574
|
if (positionals[0] === 'settings') result = { ...(input ? save_launch_settings(home, input) : launch_settings(home)), api_providers: API_PROVIDERS,
|
|
1491
1575
|
scheduler_providers: SCHEDULER_PROVIDERS, scheduler_modes: SCHEDULER_MODES, scheduler_default_mode: DEFAULT_SCHEDULER_MODE, api_keys: saved_api_keys(home), projects: launch_projects(home).projects, ...expert_presets(home), workflows: list_workflows(home) };
|
|
@@ -1516,8 +1600,16 @@ export async function launch_main(args = process.argv.slice(2), input_text) {
|
|
|
1516
1600
|
else if (positionals[0] === 'room-restore' && input) result = await restore_room(home, input);
|
|
1517
1601
|
else if (positionals[0] === 'room-fork' && input) result = await fork_room(home, input);
|
|
1518
1602
|
else if (positionals[0] === 'pick-directory') result = await pick_directory();
|
|
1603
|
+
else if (positionals[0] === 'browse-directory') result = list_remote_directories(input || {}, { projects: launch_projects(home).projects, home });
|
|
1604
|
+
// Device trust from this computer's own screen; other devices reach the same actions through the relay instead.
|
|
1605
|
+
else if (positionals[0] === 'device-pairing' && !values.remote) {
|
|
1606
|
+
// The QR code opens the paired website, where the new device signs in and pairs.
|
|
1607
|
+
const site = input?.site || Object.keys((await import('./remote_launch.mjs')).machine_links(home).sites)[0];
|
|
1608
|
+
result = start_pairing(home, { site, temporary: input?.temporary === true });
|
|
1609
|
+
}
|
|
1610
|
+
else if (positionals[0] === 'devices' && !values.remote) result = input?.revoke ? revoke_device(home, String(input.revoke))
|
|
1611
|
+
: input?.revoke_write ? revoke_write(home, String(input.revoke_write.device_id), String(input.revoke_write.room_id)) : list_devices(home);
|
|
1519
1612
|
else if (positionals[0] === 'machine-link' && input) result = (await import('./remote_launch.mjs')).machine_link(home, input.site);
|
|
1520
|
-
else if (positionals[0] === 'admin-account' && input) result = (await import('./admin_account.mjs')).save_admin_account(home, input);
|
|
1521
1613
|
else if (positionals[0] === 'start' && input) result = await launch_discussion(home, input, { dry_run: values['dry-run'], browser_request: values.browser });
|
|
1522
1614
|
else if (positionals[0] === 'room-config' && input) result = await room_configuration(home, input, { local_guard: values['configuration-locked'] });
|
|
1523
1615
|
else throw new Error(HELP);
|
|
@@ -1528,7 +1620,10 @@ if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.m
|
|
|
1528
1620
|
const args = process.argv.slice(2);
|
|
1529
1621
|
const stdin = args.includes('--stdin') ? readFileSync(0, 'utf8') : undefined;
|
|
1530
1622
|
launch_main(args.filter(arg => arg !== '--stdin'), stdin).catch(error => {
|
|
1531
|
-
|
|
1623
|
+
// A request that needs the page to do something first (pair, renew, confirm writing) says so in a field it can read.
|
|
1624
|
+
const need = error.need ? { need: error.need, ...(error.room_id ? { room_id: error.room_id } : {}) } : {};
|
|
1625
|
+
const failure = args.includes('--browser') ? JSON.stringify({ error: error.message, ...need, launch: error.launch || { stage: 'not_started' } })
|
|
1626
|
+
: error.need ? JSON.stringify({ error: error.message, ...need }) : error.message;
|
|
1532
1627
|
process.stderr.write(`${failure}\n`); process.exitCode = 1;
|
|
1533
1628
|
});
|
|
1534
1629
|
}
|
|
@@ -688,6 +688,7 @@ export async function run_service(home, output = () => {}) {
|
|
|
688
688
|
// Loaded lazily: the relay runs launches, and the launch module itself depends on this one.
|
|
689
689
|
const { relay_commands, sync_remote_launches } = await import('./remote_launch.mjs');
|
|
690
690
|
const { start_relay_channels } = await import('./relay_channel.mjs');
|
|
691
|
+
if ((await import('./admin_account.mjs')).remove_legacy_admin_accounts(home)) log_line('已删除旧版本明文保存的管理员密码文件 admin-accounts.json');
|
|
691
692
|
channels = start_relay_channels(home, { log: log_line, signal: relay_stop.signal });
|
|
692
693
|
let synced_at = 0, swept_at = 0, relay_failed_at = 0;
|
|
693
694
|
// A long-poll of its own: the site answers as soon as another device relays a command, and the next claim starts
|
|
@@ -749,7 +750,7 @@ export async function start_service(home) {
|
|
|
749
750
|
if (existsSync(stop_path)) unlinkSync(stop_path);
|
|
750
751
|
const log = openSync(join(home, 'service.log'), 'a');
|
|
751
752
|
child = spawn(process.execPath, [SCRIPT, 'run'], { detached: true, windowsHide: true,
|
|
752
|
-
stdio: ['ignore', log, log], env: { ...
|
|
753
|
+
stdio: ['ignore', log, log], env: { ...persisted_env(), DISCUSS_SERVICE_HOME: home } });
|
|
753
754
|
closeSync(log);
|
|
754
755
|
child.once('error', error => { spawn_error = error; });
|
|
755
756
|
child.unref();
|
|
@@ -766,6 +767,29 @@ export async function start_service(home) {
|
|
|
766
767
|
throw new Error(`讨论服务尚未确认就绪,请查看 ${join(home, 'service.log')}`);
|
|
767
768
|
}
|
|
768
769
|
|
|
770
|
+
/**
|
|
771
|
+
* This process's environment with the variables saved in the Windows registry (Machine, then User) laid over it, so a
|
|
772
|
+
* service started or restarted from a terminal opened before `setx` still sees the new values. PATH is kept as
|
|
773
|
+
* inherited; a variable only deleted from the registry is not removed. Other systems, or a failed read, keep it as is.
|
|
774
|
+
*/
|
|
775
|
+
export function persisted_env(env = process.env, read = read_persisted_env) {
|
|
776
|
+
const merged = { ...env };
|
|
777
|
+
for (const [key, value] of Object.entries(read())) {
|
|
778
|
+
if (/^path$/i.test(key) || typeof value !== 'string') continue;
|
|
779
|
+
for (const existing of Object.keys(merged)) if (existing.toLowerCase() === key.toLowerCase()) delete merged[existing];
|
|
780
|
+
merged[key] = value;
|
|
781
|
+
}
|
|
782
|
+
return merged;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function read_persisted_env() {
|
|
786
|
+
if (process.platform !== 'win32') return {};
|
|
787
|
+
try {
|
|
788
|
+
return JSON.parse(powershell("$r=@{}; foreach($s in 'Machine','User'){ $v=[Environment]::GetEnvironmentVariables($s);"
|
|
789
|
+
+ " foreach($k in $v.Keys){ $r[$k]=$v[$k] } }; $r | ConvertTo-Json -Compress") || '{}');
|
|
790
|
+
} catch { return {}; }
|
|
791
|
+
}
|
|
792
|
+
|
|
769
793
|
/** Ask the service to pause its runs and exit, then wait for its lock to be released. */
|
|
770
794
|
async function stop_service(home) {
|
|
771
795
|
if (!service_pid(home)) return { running: false };
|
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prompts for one expert, beside the shared skills: a skill holds rules for everyone, this holds what one
|
|
3
|
-
* software or one member needs. Presets exist only where a software needs one; a member's own
|
|
4
|
-
* system_prompt, even an empty one, replaces its preset. They add guidance and never widen permissions.
|
|
5
|
-
*/
|
|
6
|
-
// Plain data with no imports of its own, so this module stays loadable by the web page.
|
|
7
|
-
import { adapter_profile } from './adapters/profiles.mjs';
|
|
8
|
-
|
|
9
|
-
export const MAX_EXPERT_PROMPT = 4000;
|
|
10
|
-
|
|
11
|
-
// A software's must-know rules are fixed by the runner (its adapter profile's task_rules, API in its system prompt), so no preset is needed now.
|
|
12
|
-
export const EXPERT_PROMPT_PRESETS = {};
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Expert presets: a named instance with its description, prompt, project access and optionally model and effort,
|
|
16
|
-
* added in one click. The user's own presets live on this computer (`expert-presets.json` in the service home);
|
|
17
|
-
* one with a built-in's name replaces it.
|
|
18
|
-
*/
|
|
19
|
-
export const BUILTIN_EXPERT_PRESETS = [
|
|
20
|
-
{ name: 'Codex-review', label: 'Codex-评审', builtin: true, project_access: 'read',
|
|
21
|
-
description: '专门做代码评审:对照计划文档检查改动,只提问题不改代码。',
|
|
22
|
-
system_prompt: '你是本房间的评审。先读主持人给出的计划文档与改动范围(执行报告列有改动文件,可用 git diff 查看),检查:是否符合计划与约定的接口、有无正确性问题、有无越出分工的改动、测试是否覆盖关键行为。按严重程度列出问题,每条附文件与行号和理由;没有问题就明确说通过。只评审,不修改文件。' },
|
|
23
|
-
{ name: 'Antigravity-docs', label: 'Gemini-文档', builtin: true, project_access: 'write',
|
|
24
|
-
description: '专门整理文档:按改动同步说明,只改文档不改代码。',
|
|
25
|
-
system_prompt: '你负责整理文档。先读计划文档与本次改动,按仓库现有文档的分工同步说明,只改文档文件,不改代码;不确定的事实标为待核实,不自行补写。' },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
/** The prompt this member receives: its own text when set, otherwise its software's preset. */
|
|
29
|
-
export function expert_prompt(profile = {}) {
|
|
30
|
-
const own = Object.hasOwn(profile, 'system_prompt');
|
|
31
|
-
return String((own ? profile.system_prompt : EXPERT_PROMPT_PRESETS[profile.adapter]) || '').trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** The line that introduces the member's own prompt after the fixed task text; the prompt dialog shows the same line. */
|
|
35
|
-
export const EXPERT_PROMPT_HEADING = '用户为你单独设置的提示(补充做法与偏好,不改变权限与技能规则):';
|
|
36
|
-
|
|
37
|
-
/** Append the member's prompt to a task prompt, marked as the user's own guidance for this member. */
|
|
38
|
-
export function with_expert_prompt(prompt, profile) {
|
|
39
|
-
const extra = expert_prompt(profile);
|
|
40
|
-
return extra ? `${prompt}\n\n${EXPERT_PROMPT_HEADING}\n${extra}` : prompt;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/*
|
|
44
|
-
* The fixed task text the runner adds for a member woken on this computer. It lives here, free of Node modules, so the
|
|
45
|
-
* expert prompt dialog shows the same text the runner sends, with placeholders for the values known only at delivery.
|
|
46
|
-
*/
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Project access of a member woken on this computer: 只读, 可写 (project files and any command), 自动 (the software's own
|
|
50
|
-
* automatic approval) and 全放开 (no approval or sandbox). A manual member the service took over is `own`.
|
|
51
|
-
*/
|
|
52
|
-
export const PROJECT_ACCESS = ['read', 'write', 'auto', 'full'];
|
|
53
|
-
|
|
54
|
-
const ACCESS_LABELS = { read: '只读', write: '可写', auto: '自动', full: '全放开' };
|
|
55
|
-
|
|
56
|
-
/** The levels a software can enforce headless, from its adapter profile; an App or API member is offered all of them. */
|
|
57
|
-
export function access_levels(adapter) {
|
|
58
|
-
return adapter_profile(adapter)?.access_levels || PROJECT_ACCESS;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* All four levels labelled for the page, with what each does in this software on hover; ones it lacks are shown but
|
|
63
|
-
* disabled, so every CLI card lines up the same four levels.
|
|
64
|
-
*/
|
|
65
|
-
export function access_options(adapter) {
|
|
66
|
-
const levels = access_levels(adapter), profile = adapter_profile(adapter);
|
|
67
|
-
return PROJECT_ACCESS.map(value => levels.includes(value)
|
|
68
|
-
? { value, label: ACCESS_LABELS[value], hint: profile?.access_hints?.[value] || '' }
|
|
69
|
-
: { value, label: ACCESS_LABELS[value], hint: profile?.unsupported_hints?.[value] || '该软件不支持此档位', disabled: true });
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** The page's name for a level; `own` reads as decided by the member's own session. */
|
|
73
|
-
export function access_label(level) { return level === 'own' ? '自行决定' : ACCESS_LABELS[level] || '只读'; }
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Subcommands an automatic task may run; service, run, api, setup and the rest of launch stay unapproved.
|
|
77
|
-
* `launch workflow-adopt` checks itself that the caller is the host of a room whose owner allowed it.
|
|
78
|
-
*/
|
|
79
|
-
export const TASK_SUBCOMMANDS = ['room', 'message', 'turn', 'state', 'review', 'execution', 'health', 'schema', 'launch workflow-adopt'];
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Whether a member may run commands beyond the codiscuss subcommands: every level but 只读, and 可写 only with a project.
|
|
83
|
-
* A taken-over manual member (`own`) keeps its own session's permissions, so the room's limits do not apply.
|
|
84
|
-
*/
|
|
85
|
-
export function runs_commands(config) {
|
|
86
|
-
const access = config.project_access;
|
|
87
|
-
return ['own', 'auto', 'full'].includes(access) || (access === 'write' && Boolean(config.cwd || config.workdir));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/** The task text's rule for project files and commands; `purpose` says what the member works for (按请求 / 按议题需要). */
|
|
91
|
-
export function project_rule(profile, purpose) {
|
|
92
|
-
if (profile.project_access === 'own') return '项目文件与命令权限沿用你自己会话的软件设置。';
|
|
93
|
-
if (profile.project_access === 'auto') return `由软件自动审批:可${purpose}修改项目文件并运行所需命令;被沙箱或审批拦下时按软件方式申请放行(Codex 申请提权),仍被拒绝就在回答中说明。`;
|
|
94
|
-
if (profile.project_access === 'full') return `已跳过软件审批与沙箱,可${purpose}修改文件并运行所需命令。`;
|
|
95
|
-
if (!profile.cwd) return '未指定项目目录。';
|
|
96
|
-
return profile.project_access === 'write' ? `用户允许${purpose}修改 cwd 中的项目文件并运行所需命令。` : 'cwd 中的项目文件只读,不修改。';
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The fixed rules one software needs in every task, from its adapter profile; no member prompt can drop them. */
|
|
100
|
-
export function software_rules(config, cli) {
|
|
101
|
-
return adapter_profile(config.adapter)?.task_rules?.(cli, { runs_commands: runs_commands(config) }) || '';
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Windows PowerShell mis-decodes UTF-8 files; the runner adds this where the member's commands can run rg. */
|
|
105
|
-
export const READ_RULE = '读含中文的文件用 rg --encoding utf-8 . 文件路径,不用 Get-Content(Windows PowerShell 会读成乱码)。';
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* A free-room task's fixed text: `instructions` saved in the task file and `prompt` sent with each wake-up, followed by
|
|
109
|
-
* the member's own prompt. `discussion` is a direct question asked while a discussion runs in the room;
|
|
110
|
-
* `discussion_request` is the session request for a scheduled speaker or the host.
|
|
111
|
-
* Loaded in steps: the prompt says who was named, how to answer a simple message without loading the skills, and
|
|
112
|
-
* otherwise where the task file is, plus how to read files at all; the task file's fields carry this execution's values
|
|
113
|
-
* (messages, read point, execution, role, credentials), the skills it lists say how to use them, and `instructions`
|
|
114
|
-
* holds only the limits no skill can know. `access_file` is set where the credential is not bound to the process.
|
|
115
|
-
*/
|
|
116
|
-
export function free_task_text({ name, discussion, discussion_request, profile, cli, messages, read_after, task_path, access_file = '', read_rule = '' }) {
|
|
117
|
-
const scope = discussion ? `讨论进行中,用户直接向你提问(${messages}):只回答这个提问,不改变讨论安排和进度。` : '';
|
|
118
|
-
const commands = runs_commands(profile) ? '' : `命令只用 subcommands 列出的 ${cli} 子命令。`;
|
|
119
|
-
const instructions = `${software_rules(profile, cli)}${scope}${project_rule(profile, '按请求')}${commands}草稿写在 draft_directory。`;
|
|
120
|
-
if (discussion_request) {
|
|
121
|
-
const role = discussion_request.purpose === 'discuss_host' ? '主持任务' : '发言请求';
|
|
122
|
-
const prompt = `你(${name})收到了讨论中的${role}。先读任务文件 ${task_path},按其中列出的技能处理本次请求并保存结果。${read_rule}`;
|
|
123
|
-
return { instructions, prompt };
|
|
124
|
-
}
|
|
125
|
-
// A greeting deserves a quick answer, and reading the skills first costs it one or two model rounds.
|
|
126
|
-
const access = access_file ? ` --access-file ${access_file}` : '';
|
|
127
|
-
// A one-line answer goes inline: a draft file guards long or quoted text and retries, which a greeting does not need.
|
|
128
|
-
// The credential already names the room, the member and this execution, so the commands carry only what changes.
|
|
129
|
-
const quick = `问候、致谢等简单消息可跳过技能:${cli} message list --after ${read_after}${access} 看原文,${cli} message post --body "回答"${access} 回复(正文含引号时先写草稿再用 --body-file)。`;
|
|
130
|
-
const prompt = `用户在${discussion ? '讨论' : '自由协作'}房间点名了你(${name}),对应消息 ${messages}。${quick}其他请求先读任务文件 ${task_path},按其中列出的技能处理并提交一次回答。${read_rule}`;
|
|
131
|
-
return { instructions, prompt };
|
|
132
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Prompts for one expert, beside the shared skills: a skill holds rules for everyone, this holds what one
|
|
3
|
+
* software or one member needs. Presets exist only where a software needs one; a member's own
|
|
4
|
+
* system_prompt, even an empty one, replaces its preset. They add guidance and never widen permissions.
|
|
5
|
+
*/
|
|
6
|
+
// Plain data with no imports of its own, so this module stays loadable by the web page.
|
|
7
|
+
import { adapter_profile } from './adapters/profiles.mjs';
|
|
8
|
+
|
|
9
|
+
export const MAX_EXPERT_PROMPT = 4000;
|
|
10
|
+
|
|
11
|
+
// A software's must-know rules are fixed by the runner (its adapter profile's task_rules, API in its system prompt), so no preset is needed now.
|
|
12
|
+
export const EXPERT_PROMPT_PRESETS = {};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Expert presets: a named instance with its description, prompt, project access and optionally model and effort,
|
|
16
|
+
* added in one click. The user's own presets live on this computer (`expert-presets.json` in the service home);
|
|
17
|
+
* one with a built-in's name replaces it.
|
|
18
|
+
*/
|
|
19
|
+
export const BUILTIN_EXPERT_PRESETS = [
|
|
20
|
+
{ name: 'Codex-review', label: 'Codex-评审', builtin: true, project_access: 'read',
|
|
21
|
+
description: '专门做代码评审:对照计划文档检查改动,只提问题不改代码。',
|
|
22
|
+
system_prompt: '你是本房间的评审。先读主持人给出的计划文档与改动范围(执行报告列有改动文件,可用 git diff 查看),检查:是否符合计划与约定的接口、有无正确性问题、有无越出分工的改动、测试是否覆盖关键行为。按严重程度列出问题,每条附文件与行号和理由;没有问题就明确说通过。只评审,不修改文件。' },
|
|
23
|
+
{ name: 'Antigravity-docs', label: 'Gemini-文档', builtin: true, project_access: 'write',
|
|
24
|
+
description: '专门整理文档:按改动同步说明,只改文档不改代码。',
|
|
25
|
+
system_prompt: '你负责整理文档。先读计划文档与本次改动,按仓库现有文档的分工同步说明,只改文档文件,不改代码;不确定的事实标为待核实,不自行补写。' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** The prompt this member receives: its own text when set, otherwise its software's preset. */
|
|
29
|
+
export function expert_prompt(profile = {}) {
|
|
30
|
+
const own = Object.hasOwn(profile, 'system_prompt');
|
|
31
|
+
return String((own ? profile.system_prompt : EXPERT_PROMPT_PRESETS[profile.adapter]) || '').trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The line that introduces the member's own prompt after the fixed task text; the prompt dialog shows the same line. */
|
|
35
|
+
export const EXPERT_PROMPT_HEADING = '用户为你单独设置的提示(补充做法与偏好,不改变权限与技能规则):';
|
|
36
|
+
|
|
37
|
+
/** Append the member's prompt to a task prompt, marked as the user's own guidance for this member. */
|
|
38
|
+
export function with_expert_prompt(prompt, profile) {
|
|
39
|
+
const extra = expert_prompt(profile);
|
|
40
|
+
return extra ? `${prompt}\n\n${EXPERT_PROMPT_HEADING}\n${extra}` : prompt;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* The fixed task text the runner adds for a member woken on this computer. It lives here, free of Node modules, so the
|
|
45
|
+
* expert prompt dialog shows the same text the runner sends, with placeholders for the values known only at delivery.
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Project access of a member woken on this computer: 只读, 可写 (project files and any command), 自动 (the software's own
|
|
50
|
+
* automatic approval) and 全放开 (no approval or sandbox). A manual member the service took over is `own`.
|
|
51
|
+
*/
|
|
52
|
+
export const PROJECT_ACCESS = ['read', 'write', 'auto', 'full'];
|
|
53
|
+
|
|
54
|
+
const ACCESS_LABELS = { read: '只读', write: '可写', auto: '自动', full: '全放开' };
|
|
55
|
+
|
|
56
|
+
/** The levels a software can enforce headless, from its adapter profile; an App or API member is offered all of them. */
|
|
57
|
+
export function access_levels(adapter) {
|
|
58
|
+
return adapter_profile(adapter)?.access_levels || PROJECT_ACCESS;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* All four levels labelled for the page, with what each does in this software on hover; ones it lacks are shown but
|
|
63
|
+
* disabled, so every CLI card lines up the same four levels.
|
|
64
|
+
*/
|
|
65
|
+
export function access_options(adapter) {
|
|
66
|
+
const levels = access_levels(adapter), profile = adapter_profile(adapter);
|
|
67
|
+
return PROJECT_ACCESS.map(value => levels.includes(value)
|
|
68
|
+
? { value, label: ACCESS_LABELS[value], hint: profile?.access_hints?.[value] || '' }
|
|
69
|
+
: { value, label: ACCESS_LABELS[value], hint: profile?.unsupported_hints?.[value] || '该软件不支持此档位', disabled: true });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The page's name for a level; `own` reads as decided by the member's own session. */
|
|
73
|
+
export function access_label(level) { return level === 'own' ? '自行决定' : ACCESS_LABELS[level] || '只读'; }
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Subcommands an automatic task may run; service, run, api, setup and the rest of launch stay unapproved.
|
|
77
|
+
* `launch workflow-adopt` checks itself that the caller is the host of a room whose owner allowed it.
|
|
78
|
+
*/
|
|
79
|
+
export const TASK_SUBCOMMANDS = ['room', 'message', 'turn', 'state', 'goal', 'mode', 'review', 'execution', 'health', 'schema', 'launch workflow-adopt'];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether a member may run commands beyond the codiscuss subcommands: every level but 只读, and 可写 only with a project.
|
|
83
|
+
* A taken-over manual member (`own`) keeps its own session's permissions, so the room's limits do not apply.
|
|
84
|
+
*/
|
|
85
|
+
export function runs_commands(config) {
|
|
86
|
+
const access = config.project_access;
|
|
87
|
+
return ['own', 'auto', 'full'].includes(access) || (access === 'write' && Boolean(config.cwd || config.workdir));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The task text's rule for project files and commands; `purpose` says what the member works for (按请求 / 按议题需要). */
|
|
91
|
+
export function project_rule(profile, purpose) {
|
|
92
|
+
if (profile.project_access === 'own') return '项目文件与命令权限沿用你自己会话的软件设置。';
|
|
93
|
+
if (profile.project_access === 'auto') return `由软件自动审批:可${purpose}修改项目文件并运行所需命令;被沙箱或审批拦下时按软件方式申请放行(Codex 申请提权),仍被拒绝就在回答中说明。`;
|
|
94
|
+
if (profile.project_access === 'full') return `已跳过软件审批与沙箱,可${purpose}修改文件并运行所需命令。`;
|
|
95
|
+
if (!profile.cwd) return '未指定项目目录。';
|
|
96
|
+
return profile.project_access === 'write' ? `用户允许${purpose}修改 cwd 中的项目文件并运行所需命令。` : 'cwd 中的项目文件只读,不修改。';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The fixed rules one software needs in every task, from its adapter profile; no member prompt can drop them. */
|
|
100
|
+
export function software_rules(config, cli) {
|
|
101
|
+
return adapter_profile(config.adapter)?.task_rules?.(cli, { runs_commands: runs_commands(config) }) || '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Windows PowerShell mis-decodes UTF-8 files; the runner adds this where the member's commands can run rg. */
|
|
105
|
+
export const READ_RULE = '读含中文的文件用 rg --encoding utf-8 . 文件路径,不用 Get-Content(Windows PowerShell 会读成乱码)。';
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* A free-room task's fixed text: `instructions` saved in the task file and `prompt` sent with each wake-up, followed by
|
|
109
|
+
* the member's own prompt. `discussion` is a direct question asked while a discussion runs in the room;
|
|
110
|
+
* `discussion_request` is the session request for a scheduled speaker or the host.
|
|
111
|
+
* Loaded in steps: the prompt says who was named, how to answer a simple message without loading the skills, and
|
|
112
|
+
* otherwise where the task file is, plus how to read files at all; the task file's fields carry this execution's values
|
|
113
|
+
* (messages, read point, execution, role, credentials), the skills it lists say how to use them, and `instructions`
|
|
114
|
+
* holds only the limits no skill can know. `access_file` is set where the credential is not bound to the process.
|
|
115
|
+
*/
|
|
116
|
+
export function free_task_text({ name, discussion, discussion_request, profile, cli, messages, read_after, task_path, access_file = '', read_rule = '' }) {
|
|
117
|
+
const scope = discussion ? `讨论进行中,用户直接向你提问(${messages}):只回答这个提问,不改变讨论安排和进度。` : '';
|
|
118
|
+
const commands = runs_commands(profile) ? '' : `命令只用 subcommands 列出的 ${cli} 子命令。`;
|
|
119
|
+
const instructions = `${software_rules(profile, cli)}${scope}${project_rule(profile, '按请求')}${commands}草稿写在 draft_directory。`;
|
|
120
|
+
if (discussion_request) {
|
|
121
|
+
const role = discussion_request.purpose === 'discuss_host' ? '主持任务' : '发言请求';
|
|
122
|
+
const prompt = `你(${name})收到了讨论中的${role}。先读任务文件 ${task_path},按其中列出的技能处理本次请求并保存结果。${read_rule}`;
|
|
123
|
+
return { instructions, prompt };
|
|
124
|
+
}
|
|
125
|
+
// A greeting deserves a quick answer, and reading the skills first costs it one or two model rounds.
|
|
126
|
+
const access = access_file ? ` --access-file ${access_file}` : '';
|
|
127
|
+
// A one-line answer goes inline: a draft file guards long or quoted text and retries, which a greeting does not need.
|
|
128
|
+
// The credential already names the room, the member and this execution, so the commands carry only what changes.
|
|
129
|
+
const quick = `问候、致谢等简单消息可跳过技能:${cli} message list --after ${read_after}${access} 看原文,${cli} message post --body "回答"${access} 回复(正文含引号时先写草稿再用 --body-file)。`;
|
|
130
|
+
const prompt = `用户在${discussion ? '讨论' : '自由协作'}房间点名了你(${name}),对应消息 ${messages}。${quick}其他请求先读任务文件 ${task_path},按其中列出的技能处理并提交一次回答。${read_rule}`;
|
|
131
|
+
return { instructions, prompt };
|
|
132
|
+
}
|