CLI(bun 跑源码即生效): - boot:loadBootData 一次性加载模型清单(含 context_window)/个性化默认 (含压缩设置)/path 授权/会话列表;新会话 unshift 标 current - /session:Enter 真实加载(timeline.reset + getSessionHistory 渲染历史文本) - /new:对齐 web /new 页面语义——空对话草稿不立即建会话,首条消息 createTask 不带 cid 由服务端惰性创建,响应回 id 后插入列表标 current - /context + 状态栏:token_update 事件(运行期事件流)+ 打开面板时查询 新端点 GET /api/runtime/sessions/<cid>/token-stats;百分比算法对齐 web InputComposer(自动压缩阈值优先于模型窗口) - /model:Enter = 本地生效 + createTask 带 model_key/run_mode/thinking_mode 覆盖(会话级即时)+ savePersonalization patch 存默认(effort default 存 null) - /path:增删后 savePathAuths 全量提交两组(服务端 deny 校验保留) - /agents /tasks /workflow /rewind:打开时拉真实列表(sub_agents/ background_commands/workflows/versioning-checkpoints) - 发送链路统一 sendWithSettings 携带 /model 生效值(含排队队列) 后端: - 8 端点装饰器 api_login_required → api_login_or_host_token_required (personalization×2 / path-authorization×2 / sub_agents / background_commands / workflows / versioning-checkpoints),web session 行为不变 - headless_app 用 add_url_rule 共享上述视图函数(URL 与 full 一致); sub_agents/background_commands 支持 query 显式传 conversation_id (Bearer 装配的 terminal 无当前对话概念) - RuntimeService 新增 get_session_token_stats;gateway_api 挂 token-stats 路由 - headless 404 兜底:非 /api/ 的 GET 路径回落告知页(对齐 SPA fallback) 验证:tsc ✅ py_compile ✅ 冒烟 6/6 ✅ headless 装配 54 路由 ✅ localconfig 无头实测读到真实配置 ✅;真实服务端到端待用户重启 8091 实机验收 Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
90 lines
3.9 KiB
TypeScript
90 lines
3.9 KiB
TypeScript
// 本地配置读取(仅 host 本机单人模式):与 host_api_token 同一安全语义——本机信任;
|
||
// 服务端读的也是同一份文件(resolve_deploy_config 回退链 / personalization manager),读一致性有保证。
|
||
// 边界:本模块只读;写操作(保存模型/增删路径授权)必须走 Gateway API,不在此列。
|
||
import { homedir } from 'node:os';
|
||
import { readFileSync, existsSync } from 'node:fs';
|
||
import { resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import type { EffortLevel, ModelOption, PathAuth } from './data';
|
||
|
||
const DATA_ROOT = `${homedir()}/.astrion/astrion`;
|
||
const DEPLOY_CONFIG_DIR = `${DATA_ROOT}/config`;
|
||
const HOST_DATA_DIR = `${DATA_ROOT}/host/data`;
|
||
/** 源码树(cli/src → 项目根;fileURLToPath 正确解码中文路径) */
|
||
const REPO_ROOT = resolve(fileURLToPath(new URL('../..', import.meta.url)));
|
||
|
||
function readJson(path: string): any | null {
|
||
try {
|
||
return JSON.parse(readFileSync(path, 'utf8'));
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/** 部署级配置回退链(对齐 config.resolve_deploy_config):部署目录 → 源码树 */
|
||
function resolveDeployConfig(name: string): any | null {
|
||
const deployPath = resolve(DEPLOY_CONFIG_DIR, name);
|
||
if (existsSync(deployPath)) return readJson(deployPath);
|
||
const repoPath = resolve(REPO_ROOT, 'config', name);
|
||
if (existsSync(repoPath)) return readJson(repoPath);
|
||
return null;
|
||
}
|
||
|
||
/** 模型清单(custom_models.json;visible=false 的条目不上架;context_window 透传供上下文百分比) */
|
||
export function loadCustomModels(): ModelOption[] {
|
||
const cfg = resolveDeployConfig('custom_models.json');
|
||
const items = Array.isArray(cfg?.models) ? cfg.models : [];
|
||
return items
|
||
.filter((m: any) => m && m.model_name && m.visible !== false)
|
||
.map((m: any) => ({
|
||
name: String(m.model_name),
|
||
meta: String(m.model_description || m.description || ''),
|
||
contextWindow: typeof m.context_window === 'number' && m.context_window > 0 ? m.context_window : null,
|
||
}));
|
||
}
|
||
|
||
export interface BootPreferences {
|
||
model: string;
|
||
/** default_run_mode:fast/thinking/deep → thinking 布尔(面板显示用) */
|
||
thinking: boolean;
|
||
runMode: string;
|
||
effort: EffortLevel;
|
||
workMode: string;
|
||
permMode: string;
|
||
/** 自动深度压缩开关与触发阈值(上下文百分比分母,对齐 web 端算法) */
|
||
autoDeepCompress: boolean;
|
||
deepCompressLimit: number;
|
||
}
|
||
|
||
const EFFORT_VALUES: EffortLevel[] = ['default', 'low', 'medium', 'high', 'xhigh', 'max'];
|
||
|
||
/** 个性化默认(personalization.json:默认模型/运行模式/推理强度/工作模式/权限模式/压缩设置) */
|
||
export function loadPersonalizationDefaults(): BootPreferences {
|
||
const p = readJson(resolve(HOST_DATA_DIR, 'personalization.json')) ?? {};
|
||
const effortRaw = String(p.default_reasoning_effort ?? 'default');
|
||
const limitRaw = Number(p.deep_compress_trigger_tokens);
|
||
return {
|
||
model: String(p.default_model ?? ''),
|
||
runMode: String(p.default_run_mode ?? 'fast'),
|
||
thinking: String(p.default_run_mode ?? 'fast') !== 'fast',
|
||
effort: (EFFORT_VALUES as string[]).includes(effortRaw) ? (effortRaw as EffortLevel) : 'default',
|
||
workMode: String(p.default_work_mode ?? 'ask'),
|
||
permMode: String(p.default_permission_mode ?? 'unrestricted'),
|
||
autoDeepCompress: !!p.auto_deep_compress_enabled,
|
||
deepCompressLimit: Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : 150000,
|
||
};
|
||
}
|
||
|
||
/** 路径授权(host_sandbox_policy.json:writable→rw 组 + readable_extra→ro 组) */
|
||
export function loadPathAuths(): PathAuth[] {
|
||
const cfg = resolveDeployConfig('host_sandbox_policy.json') ?? {};
|
||
const out: PathAuth[] = [];
|
||
for (const p of cfg.macos_writable_paths ?? []) {
|
||
if (typeof p === 'string' && p.trim()) out.push({ path: p, access: 'rw' });
|
||
}
|
||
for (const p of cfg.macos_readable_extra_paths ?? []) {
|
||
if (typeof p === 'string' && p.trim()) out.push({ path: p, access: 'ro' });
|
||
}
|
||
return out;
|
||
}
|