agent-Specialization/cli/src/localconfig.ts
JOJO 5459d82637 feat(cli): 面板与状态栏接真实数据,/new 对齐草稿语义;host 设置面双通道化
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>
2026-09-11 21:20:58 +08:00

90 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 本地配置读取(仅 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.jsonvisible=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_modefast/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.jsonwritable→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;
}