Compare commits

..

No commits in common. "6a2cb820bcd5c81585e3227e7eb2624c16637642" and "2264a86851df6fbbfcdb7702992e47ee6763216a" have entirely different histories.

38 changed files with 102 additions and 425 deletions

View File

@ -1,48 +0,0 @@
---
name: astrion_naming
description: 当需要了解或引用本项目的品牌名称、产品名 Astrion 的含义与来源时,应该索引本记忆
---
# 项目名称 / 产品名Astrion
## 决策记录
- **暂定名称**Astrion
- **决策时间**2026-07-07
- **命名方向**:人格化 + 宇宙现象/天体 + 科技意象
- **当前状态**暂定后续如需变更请同步更新本记忆、AGENTS.md 与 claude.md
## 名称含义
**构词**
- **Astr-**:来自希腊语 *astron*,意为“星星 / 天体 / 宇宙”
- **-ion**:科技命名中常见的粒子/状态后缀(如 photon、electron、neutron、ion
**字面意象**
> 星之子、星际粒子、来自宇宙尘埃的微小发光体。
## 表达特点
| 特点 | 解读 |
|------|------|
| 科技感 | `-ion` 后缀自带物理/科技联想 |
| 宇宙感 | `Astr-` 词根直接指向星空 |
| 轻盈感 | 粒子暗示微小、快速、自由穿行 |
| 能量感 | ion离子带电暗示活跃、驱动、连接 |
| 可人格化 | 三音节 `/ˈæstriən/`,发音接近 Orion、Damion适合作为“他/她” |
| 中性 | 不偏男性或女性,适用面广 |
## 品牌叙事参考
1. **连接者**:像星际间传递信号的粒子,把人类意图传递到工具、文件、终端、子智能体之间。
2. **从混沌中组织秩序**宇宙尘埃在引力下聚集成恒星Astrion 把零散任务聚合成可执行结果。
3. **渺小但明亮**:单个粒子微不足道,但携带能量、能引发连锁反应——对应 Agent 虽小,却能撬动复杂系统。
## 中文参考
- 全称音译:**阿斯特里恩**
- 简称候选:**星子**(对应 astron + particle更顺口
## 同步位置
- 项目规范:`AGENTS.md`
- Claude Code 指南:`claude.md`

View File

@ -7,16 +7,6 @@
> 和用户交流时默认使用中文。 > 和用户交流时默认使用中文。
## 0) 项目名称
本项目(仓库 `agents`)的暂定产品名为 **Astrion**
- **构词**`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron
- **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。
- **寓意**Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`、`claude.md` 与项目记忆 `astrion_naming`
- **中文参考**:阿斯特里恩;简称可考虑 **星子**
## 1) 当前项目结构(按代码现状) ## 1) 当前项目结构(按代码现状)
- **后端入口** - **后端入口**

View File

@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview ## Project Overview
多用户 AI Agent 系统,产品暂定名为 **Astrion**`Astr-` 星星/天体 + `-ion` 粒子后缀,意象为“星之子、星际粒子”)。为每个登录用户提供独立的 Docker 容器环境支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。 多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
## Core Architecture ## Core Architecture

View File

@ -1,4 +1,4 @@
# Astrion # AI Agent 系统
一个功能完整的 AI 智能体系统,支持多模态交互、子智能体协作、实时终端操作和丰富的工具集成。 一个功能完整的 AI 智能体系统,支持多模态交互、子智能体协作、实时终端操作和丰富的工具集成。

15
agentskills/test/SKILL.md Normal file
View File

@ -0,0 +1,15 @@
---
name: Test Skill
description: Validate skills loading and read_file flow.
example_prompts:
- "Use the test skill to explain how to proceed."
tools:
- read_file
tags:
- test
---
# Test Skill
This skill exists to verify that the skills system can discover, list, sync,
and read SKILL.md content via read_file. Replace this content later.

View File

@ -97,7 +97,7 @@ function isWideCodePoint(code: number): boolean {
export function WelcomePanel({ status }: { status: CliStatus }) { export function WelcomePanel({ status }: { status: CliStatus }) {
return ( return (
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
<Text color="gray">Astrion</Text> <Text color="gray">Agents</Text>
<Box borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} flexDirection="column"> <Box borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} flexDirection="column">
<Text bold>Welcome back!</Text> <Text bold>Welcome back!</Text>
<Text color="gray">{status.model} · {status.runMode}</Text> <Text color="gray">{status.model} · {status.runMode}</Text>
@ -279,7 +279,7 @@ export function WorkspacePrompt({ cwd, selectedIndex }: { cwd: string; selectedI
export function StatusPanel({ status }: { status: CliStatus }) { export function StatusPanel({ status }: { status: CliStatus }) {
return ( return (
<Box borderStyle="round" borderColor="gray" paddingX={1} flexDirection="column" marginTop={1}> <Box borderStyle="round" borderColor="gray" paddingX={1} flexDirection="column" marginTop={1}>
<Text>{'>_ Astrion CLI'}</Text> <Text>{'>_ Agents CLI'}</Text>
<Text> </Text> <Text> </Text>
<StatusField label="Model" value={status.model} /> <StatusField label="Model" value={status.model} />
<StatusField label="Directory" value={shortPath(status.directory)} /> <StatusField label="Directory" value={shortPath(status.directory)} />

View File

@ -1,28 +1,4 @@
{ {
"macos_writable_paths": [], "macos_writable_paths": [],
"macos_readable_extra_paths": [], "macos_readable_extra_paths": []
"macos_deny_read_paths": [
"~/.ssh",
"~/.aws",
"~/.azure",
"~/.gcp",
"~/.google",
"~/.kube",
"~/.docker",
"~/.gnupg",
"~/.npmrc",
"~/.netrc",
"~/.pypirc",
"~/.git-credentials",
"~/.bash_history",
"~/.zsh_history",
"~/.psql_history",
"~/.mysql_history",
"~/.pgpass",
"/Library/Keychains",
"~/Library/Keychains"
],
"macos_deny_read_regexes": [
"^/.*\\.env(\\.[^/]*)?$"
]
} }

View File

@ -105,9 +105,6 @@ USER_SPACE_DIR = _runtime_dir("USER_SPACE_DIR", "users")
# API 专用用户与工作区(与网页用户隔离) # API 专用用户与工作区(与网页用户隔离)
API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users") API_USER_SPACE_DIR = _runtime_dir("API_USER_SPACE_DIR", "api/users")
# ── 用户自定义 skill 归档目录host 模式全局docker/web 模式可与 per-user 并存)──
CUSTOM_SKILLS_DIR = str(Path(RUNTIME_ROOT) / _MODE / "agentskills")
# ── 是否宿主机模式 ── # ── 是否宿主机模式 ──
IS_HOST_MODE = _MODE == "host" IS_HOST_MODE = _MODE == "host"
@ -193,7 +190,6 @@ __all__ = [
"INVITE_CODES_FILE", "INVITE_CODES_FILE",
"ADMIN_POLICY_FILE", "ADMIN_POLICY_FILE",
"API_USER_SPACE_DIR", "API_USER_SPACE_DIR",
"CUSTOM_SKILLS_DIR",
"API_USERS_DB_FILE", "API_USERS_DB_FILE",
"API_TOKENS_FILE", "API_TOKENS_FILE",
"API_USAGE_FILE", "API_USAGE_FILE",

View File

@ -112,13 +112,13 @@ class MessagesMixin:
# 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结; # 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结;
# 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。 # 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。
current_model = getattr(self, "model_key", None) current_model = getattr(self, "model_key", None)
prompt_name = "main_system_vl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system" prompt_name = "main_system_qwenvl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
model_key = getattr(self, "model_key", None) model_key = getattr(self, "model_key", None)
prompt_replacements = get_model_prompt_replacements(model_key) prompt_replacements = get_model_prompt_replacements(model_key)
def _build_main_system_prompt() -> str: def _build_main_system_prompt() -> str:
system_prompt_template = self.load_prompt(prompt_name) system_prompt_template = self.load_prompt(prompt_name)
# main_system.txt / main_system_vl.txt 仅使用 {model_description} # main_system.txt / main_system_qwenvl.txt 仅使用 {model_description}
return system_prompt_template.format( return system_prompt_template.format(
model_description=prompt_replacements.get("model_description", "") model_description=prompt_replacements.get("model_description", "")
) )

View File

@ -125,4 +125,4 @@ class PromptMixin:
if prompt_file.exists(): if prompt_file.exists():
with open(prompt_file, 'r', encoding='utf-8') as f: with open(prompt_file, 'r', encoding='utf-8') as f:
return f.read() return f.read()
return "你是 Astrion一个智能助手。" return "你是一个AI助手。"

View File

@ -26,6 +26,7 @@ try:
READONLY_RUN_COMMAND_ALLOWED, READONLY_RUN_COMMAND_ALLOWED,
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS, READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
READONLY_RUN_COMMAND_BLOCKED_TOKENS, READONLY_RUN_COMMAND_BLOCKED_TOKENS,
AGENT_SKILLS_DIR,
) )
except ImportError: except ImportError:
import sys import sys
@ -49,6 +50,7 @@ except ImportError:
READONLY_RUN_COMMAND_ALLOWED, READONLY_RUN_COMMAND_ALLOWED,
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS, READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
READONLY_RUN_COMMAND_BLOCKED_TOKENS, READONLY_RUN_COMMAND_BLOCKED_TOKENS,
AGENT_SKILLS_DIR,
) )
from modules.file_manager import FileManager from modules.file_manager import FileManager
@ -494,6 +496,9 @@ class MainTerminalToolsExecutionMixin:
return (project_root / candidate).resolve() return (project_root / candidate).resolve()
def _get_create_skill_target_root(self) -> Path: def _get_create_skill_target_root(self) -> Path:
if self._is_host_mode():
return Path(AGENT_SKILLS_DIR).expanduser().resolve()
inferred = infer_private_skills_dir(self.data_dir) inferred = infer_private_skills_dir(self.data_dir)
if inferred: if inferred:
return inferred return inferred
@ -636,12 +641,9 @@ class MainTerminalToolsExecutionMixin:
def _track_shallow_versioning(self, file_path: Any) -> None: def _track_shallow_versioning(self, file_path: Any) -> None:
"""Track a file edit for shallow versioning (conversation-scoped undo).""" """Track a file edit for shallow versioning (conversation-scoped undo)."""
conversation_id = getattr(self.context_manager, "current_conversation_id", None) conversation_id = getattr(self.context_manager, "current_conversation_id", None)
# 优先使用当前用户消息的 message_id让文件修改归属到正确的输入节点。
message_id = getattr(self.context_manager, "current_shallow_message_id", None) or conversation_id
from utils.perf_log import perf_log from utils.perf_log import perf_log
perf_log("_track_shallow_versioning called", extra={ perf_log("_track_shallow_versioning called", extra={
"conversation_id": conversation_id, "conversation_id": conversation_id,
"message_id": message_id,
"project_path": str(self.project_path), "project_path": str(self.project_path),
"data_dir": str(self.data_dir), "data_dir": str(self.data_dir),
"file_path": str(file_path), "file_path": str(file_path),
@ -655,10 +657,9 @@ class MainTerminalToolsExecutionMixin:
data_dir=self.data_dir, data_dir=self.data_dir,
conversation_id=conversation_id, conversation_id=conversation_id,
) )
manager.track_edit(file_path, message_id) manager.track_edit(file_path, conversation_id)
perf_log("_track_shallow_versioning done", extra={ perf_log("_track_shallow_versioning done", extra={
"conversation_id": conversation_id, "conversation_id": conversation_id,
"message_id": message_id,
"file_path": str(file_path), "file_path": str(file_path),
"tracked_files_count": len(getattr(manager, "_tracked_files", set())), "tracked_files_count": len(getattr(manager, "_tracked_files", set())),
}) })

View File

@ -27,7 +27,7 @@ class AgentSystem:
async def initialize(self): async def initialize(self):
"""初始化系统""" """初始化系统"""
print("\n" + "="*50) print("\n" + "="*50)
print("🤖 Astrion 启动") print("🤖 AI Agent 系统启动")
print("="*50) print("="*50)
# 1. 获取项目路径 # 1. 获取项目路径

View File

@ -7,55 +7,17 @@ from typing import Dict, List
from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path from config import HOST_SANDBOX_MACOS_WRITABLE_PATHS, deploy_config_path
# macOS 默认拒绝读取的敏感路径(支持 ~/ 前缀)。
DEFAULT_MACOS_DENY_READ_PATHS = [
"~/.ssh",
"~/.aws",
"~/.azure",
"~/.gcp",
"~/.google",
"~/.kube",
"~/.docker",
"~/.gnupg",
"~/.npmrc",
"~/.netrc",
"~/.pypirc",
"~/.git-credentials",
"~/.bash_history",
"~/.zsh_history",
"~/.psql_history",
"~/.mysql_history",
"~/.pgpass",
"/Library/Keychains",
"~/Library/Keychains",
]
# 默认按正则拒绝读取的敏感文件(可匹配文件系统中任意位置)。
DEFAULT_MACOS_DENY_READ_REGEXES = [
r"^/.*\.env(\.[^/]*)?$",
]
_LOCK = threading.Lock() _LOCK = threading.Lock()
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config # 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
_POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json")) _POLICY_PATH = Path(deploy_config_path("host_sandbox_policy.json"))
def _default_policy() -> Dict:
return {
"macos_writable_paths": [],
"macos_readable_extra_paths": [],
"macos_deny_read_paths": list(DEFAULT_MACOS_DENY_READ_PATHS),
"macos_deny_read_regexes": list(DEFAULT_MACOS_DENY_READ_REGEXES),
}
def _ensure_file() -> None: def _ensure_file() -> None:
if _POLICY_PATH.exists(): if _POLICY_PATH.exists():
return return
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True) _POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
_POLICY_PATH.write_text( _POLICY_PATH.write_text(
json.dumps(_default_policy(), ensure_ascii=False, indent=2), json.dumps({"macos_writable_paths": [], "macos_readable_extra_paths": []}, ensure_ascii=False, indent=2),
encoding="utf-8", encoding="utf-8",
) )
@ -66,38 +28,30 @@ def load_policy() -> Dict:
try: try:
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8")) data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
except Exception: except Exception:
data = _default_policy() data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
if not isinstance(data, dict): if not isinstance(data, dict):
data = _default_policy() data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
writable_items = data.get("macos_writable_paths")
defaults = _default_policy() if not isinstance(writable_items, list):
needs_save = False writable_items = []
for key in defaults: readable_items = data.get("macos_readable_extra_paths")
if key not in data or not isinstance(data.get(key), list): if not isinstance(readable_items, list):
data[key] = list(defaults[key]) readable_items = []
needs_save = True data["macos_writable_paths"] = [str(x).strip() for x in writable_items if str(x).strip()]
data["macos_readable_extra_paths"] = [str(x).strip() for x in readable_items if str(x).strip()]
data["macos_writable_paths"] = [str(x).strip() for x in data["macos_writable_paths"] if str(x).strip()]
data["macos_readable_extra_paths"] = [str(x).strip() for x in data["macos_readable_extra_paths"] if str(x).strip()]
data["macos_deny_read_paths"] = [str(x).strip() for x in data["macos_deny_read_paths"] if str(x).strip()]
data["macos_deny_read_regexes"] = [str(x).strip() for x in data["macos_deny_read_regexes"] if str(x).strip()]
if needs_save:
try:
_POLICY_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
except Exception:
pass
return data return data
def save_policy(policy: Dict) -> Dict: def save_policy(policy: Dict) -> Dict:
payload = dict(policy or {}) payload = dict(policy or {})
defaults = _default_policy() writable_items = payload.get("macos_writable_paths")
for key in defaults: if not isinstance(writable_items, list):
items = payload.get(key) writable_items = []
if not isinstance(items, list): readable_items = payload.get("macos_readable_extra_paths")
items = list(defaults[key]) if not isinstance(readable_items, list):
payload[key] = [str(x).strip() for x in items if str(x).strip()] readable_items = []
payload["macos_writable_paths"] = [str(x).strip() for x in writable_items if str(x).strip()]
payload["macos_readable_extra_paths"] = [str(x).strip() for x in readable_items if str(x).strip()]
with _LOCK: with _LOCK:
_ensure_file() _ensure_file()
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") _POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
@ -123,25 +77,3 @@ def get_macos_readable_paths() -> List[str]:
if val and val not in merged: if val and val not in merged:
merged.append(val) merged.append(val)
return merged return merged
def get_macos_deny_read_paths() -> List[str]:
data = load_policy()
paths = data.get("macos_deny_read_paths", [])
merged: List[str] = []
for raw in list(DEFAULT_MACOS_DENY_READ_PATHS) + list(paths or []):
val = str(raw).strip()
if val and val not in merged:
merged.append(val)
return merged
def get_macos_deny_read_regexes() -> List[str]:
data = load_policy()
patterns = data.get("macos_deny_read_regexes", [])
merged: List[str] = []
for raw in list(DEFAULT_MACOS_DENY_READ_REGEXES) + list(patterns or []):
val = str(raw).strip()
if val and val not in merged:
merged.append(val)
return merged

View File

@ -7,11 +7,7 @@ import shlex
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Dict, List, Optional
from modules.host_sandbox_policy import ( from modules.host_sandbox_policy import get_macos_writable_paths
get_macos_writable_paths,
get_macos_deny_read_paths,
get_macos_deny_read_regexes,
)
@dataclass @dataclass
@ -26,84 +22,6 @@ class HostSandboxError(RuntimeError):
pass pass
# macOS 最小可读系统路径集合Codex 风格 deny-default + allow-list
# 当前仅作为常量保留,供后续需要严格 allow-list 的只读沙箱模式使用。
# 当前只读沙箱采用“全局可读 + 敏感路径拒绝”模型,以保证工具兼容性。
MACOS_MINIMAL_READABLE_PATHS = [
"/bin",
"/sbin",
"/usr/bin",
"/usr/sbin",
"/usr/libexec",
"/usr/lib",
"/usr/share",
"/usr/local/lib",
"/opt/homebrew/lib",
"/lib",
"/etc",
"/private/etc",
"/tmp",
"/private/tmp",
"/var/tmp",
"/private/var/tmp",
"/var",
"/private/var",
"/dev",
"/System/Library/Frameworks",
"/System/Library/PrivateFrameworks",
"/System/Library/SubFrameworks",
"/System/Library/CoreServices",
"/System/Library/Extensions",
"/Library/Apple",
"/Library/Preferences",
"/Library/Filesystems/NetFSPlugins",
"/Applications",
]
def _expand_path(raw: str) -> Optional[str]:
"""展开路径中的 ~ 并返回绝对路径;无法展开时返回 None。"""
if not raw:
return None
try:
expanded = str(Path(raw).expanduser().resolve())
except Exception:
return None
return expanded
def _build_macos_read_rules(paths: List[str]) -> str:
"""把路径列表转成 (allow file-read* (subpath ...)) 规则段。"""
rules: List[str] = []
seen: set[str] = set()
for raw in paths:
expanded = _expand_path(raw)
if expanded and expanded not in seen:
seen.add(expanded)
rules.append(f'(allow file-read* (subpath "{expanded}"))')
return "\n".join(rules)
def _build_macos_deny_rules(paths: List[str]) -> str:
"""把路径列表转成 (deny file-read* (subpath ...)) 规则段。"""
rules: List[str] = []
seen: set[str] = set()
for raw in paths:
expanded = _expand_path(raw)
if expanded and expanded not in seen:
seen.add(expanded)
rules.append(f'(deny file-read* (subpath "{expanded}"))')
return "\n".join(rules)
def _build_macos_deny_regex_rules(patterns: List[str]) -> str:
"""把正则列表转成 (deny file-read* (regex #"...")) 规则段。"""
rules: List[str] = []
for pattern in patterns:
rules.append(f'(deny file-read* (regex #"{pattern}"))')
return "\n".join(rules)
# 宿主机网络权限档位 # 宿主机网络权限档位
NETWORK_PERMISSION_RESTRICTED = "restricted" # macOS: 仅本地回环Linux/Windows: 暂不隔离 NETWORK_PERMISSION_RESTRICTED = "restricted" # macOS: 仅本地回环Linux/Windows: 暂不隔离
NETWORK_PERMISSION_FULL = "full" # 完全开放 NETWORK_PERMISSION_FULL = "full" # 完全开放
@ -213,24 +131,13 @@ def _build_macos_readonly_plan(
if not sandbox_exec: if not sandbox_exec:
raise HostSandboxError("macOS 未找到 sandbox-exec拒绝执行宿主机命令。") raise HostSandboxError("macOS 未找到 sandbox-exec拒绝执行宿主机命令。")
network_policy = _build_macos_network_policy(network_permission) network_policy = _build_macos_network_policy(network_permission)
workspace = str(work_path.resolve())
# 只读沙箱:全局可读 + 敏感路径/文件拒绝,写权限仅 /dev/null。
# 工作区在 deny 规则之后再显式 allow保证“工作区内不受沙箱影响”。
deny_rules = _build_macos_deny_rules(get_macos_deny_read_paths())
regex_rules = _build_macos_deny_regex_rules(get_macos_deny_read_regexes())
if regex_rules:
deny_rules += "\n" + regex_rules
profile = ( profile = (
'(version 1)\n' '(version 1) '
'(deny default)\n' '(deny default) '
'(allow sysctl-read)\n' '(allow sysctl-read) '
'(allow process*)\n' '(allow process*) '
f'{network_policy}' f'{network_policy}'
'(allow file-read*)\n' '(allow file-read*) '
f'{deny_rules}\n'
f'(allow file-read* (subpath "{workspace}"))\n'
'(allow file-write* (literal "/dev/null"))' '(allow file-write* (literal "/dev/null"))'
) )
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command] cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
@ -271,20 +178,13 @@ def _macos_profile_for_workspace(
write_rules.append(f'(subpath "{entry}")') write_rules.append(f'(subpath "{entry}")')
write_expr = " ".join(write_rules) write_expr = " ".join(write_rules)
network_policy = _build_macos_network_policy(network_permission) network_policy = _build_macos_network_policy(network_permission)
workspace = str(work_path.resolve())
deny_rules = _build_macos_deny_rules(get_macos_deny_read_paths())
regex_rules = _build_macos_deny_regex_rules(get_macos_deny_read_regexes())
if regex_rules:
deny_rules += "\n" + regex_rules
return ( return (
'(version 1)\n' '(version 1) '
'(deny default)\n' '(deny default) '
'(allow sysctl-read)\n' '(allow sysctl-read) '
'(allow process*)\n' '(allow process*) '
f'{network_policy}' f'{network_policy}'
'(allow file-read*)\n' '(allow file-read*) '
f'{deny_rules}\n'
f'(allow file-read* (subpath "{workspace}"))\n'
f'(allow file-write* {write_expr})' f'(allow file-write* {write_expr})'
) )

View File

@ -189,14 +189,6 @@ class ShallowVersioningManager:
# ---------------------------- # ----------------------------
# public API # public API
# ---------------------------- # ----------------------------
def _find_latest_snapshot_by_message_id(self, message_id: str) -> Optional[ShallowSnapshot]:
"""Return the most recent snapshot whose message_id matches."""
target = str(message_id or "")
for snapshot in reversed(self._snapshots):
if str(snapshot.message_id or "") == target:
return snapshot
return None
def track_edit(self, file_path: Path | str, message_id: str) -> None: def track_edit(self, file_path: Path | str, message_id: str) -> None:
"""Track a file before it is edited; backup its current content if not already tracked.""" """Track a file before it is edited; backup its current content if not already tracked."""
tracking_path = self._normalize_path(file_path) tracking_path = self._normalize_path(file_path)
@ -214,13 +206,12 @@ class ShallowVersioningManager:
backup = self._create_backup(resolved, 1) backup = self._create_backup(resolved, 1)
self._tracked_files.add(tracking_path) self._tracked_files.add(tracking_path)
# 归属到当前 message_id 对应的最新 snapshot不存在则新建。 # Add to the most recent snapshot if one exists, otherwise create a pre-edit snapshot.
# 避免把本次输入的修改追加到上一个输入的 snapshot。
now = datetime.now().isoformat() now = datetime.now().isoformat()
target_snapshot = self._find_latest_snapshot_by_message_id(message_id) if self._snapshots:
if target_snapshot is not None: latest = self._snapshots[-1]
target_snapshot.tracked_file_backups[tracking_path] = backup latest.tracked_file_backups[tracking_path] = backup
self._persist_snapshot(target_snapshot) self._persist_snapshot(latest)
else: else:
snapshot = ShallowSnapshot( snapshot = ShallowSnapshot(
message_id=message_id, message_id=message_id,
@ -265,15 +256,6 @@ class ShallowVersioningManager:
tracked_file_backups=tracked_file_backups, tracked_file_backups=tracked_file_backups,
timestamp=now, timestamp=now,
) )
# 如果当前 message_id 已存在 snapshot由 track_edit 预创建),则替换它,
# 避免同一个输入节点留下多个 snapshot 导致 diff 只计算最后两次之间的变化。
existing_index = -1
for idx, s in enumerate(self._snapshots):
if str(s.message_id or "") == str(message_id or ""):
existing_index = idx
if existing_index >= 0:
self._snapshots[existing_index] = snapshot
else:
self._snapshots.append(snapshot) self._snapshots.append(snapshot)
self._snapshot_sequence += 1 self._snapshot_sequence += 1
self._persist_snapshot(snapshot) self._persist_snapshot(snapshot)

View File

@ -11,7 +11,7 @@ import threading
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Sequence from typing import Dict, List, Optional, Sequence
from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, WORKSPACE_SKILLS_DIRNAME from config import AGENT_SKILLS_DIR, WORKSPACE_SKILLS_DIRNAME
from utils.logger import setup_logger from utils.logger import setup_logger
logger = setup_logger(__name__) logger = setup_logger(__name__)
@ -48,15 +48,10 @@ def ensure_workspace_skills_dir(project_path: str | Path) -> Path:
def infer_private_skills_dir(data_dir: str | Path | None) -> Optional[Path]: def infer_private_skills_dir(data_dir: str | Path | None) -> Optional[Path]:
"""Infer per-user private skills dir from workspace data_dir. """Infer per-user private skills dir from workspace data_dir."""
host 模式下统一使用运行态根下的 ``agentskills``不再按用户拆分
"""
if not data_dir: if not data_dir:
return None return None
try: try:
if IS_HOST_MODE:
return Path(CUSTOM_SKILLS_DIR).expanduser().resolve()
data_path = Path(data_dir).expanduser().resolve() data_path = Path(data_dir).expanduser().resolve()
if data_path.name == "data" and data_path.parent.parent.name == "workspaces": if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
return (data_path.parent.parent.parent / "agentskills").resolve() return (data_path.parent.parent.parent / "agentskills").resolve()

View File

@ -1,5 +1,5 @@
{ {
"name": "astrion-frontend", "name": "ai-agent-frontend",
"version": "4.1.0", "version": "4.1.0",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@ -1,8 +1,6 @@
你是 Astrion一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
{model_description} {model_description}
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式: 你是一名运行在云端服务器/用户电脑(根据工作区信息判断)根据上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤 - **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议 - **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议

View File

@ -1,8 +1,6 @@
你是 Astrion一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
{model_description} {model_description}
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式: 你是一名运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤 - **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议 - **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议

View File

@ -207,7 +207,7 @@ def _write_custom_models(target_path: Path, entry: dict) -> Path | None:
def run_wizard(force: bool = False) -> int: def run_wizard(force: bool = False) -> int:
print("=" * 60) print("=" * 60)
print(" Astrion 首次启动初始化向导") print(" AI Agent 首次启动初始化向导")
print("=" * 60) print("=" * 60)
print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。") print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。")
print() print()
@ -311,7 +311,7 @@ def run_wizard(force: bool = False) -> int:
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description="Astrion 首次启动初始化向导") parser = argparse.ArgumentParser(description="AI Agent 首次启动初始化向导")
parser.add_argument( parser.add_argument(
"--force", "--force",
action="store_true", action="store_true",

View File

@ -35,12 +35,7 @@ from modules.skills_manager import (
sync_workspace_skills, sync_workspace_skills,
) )
from modules.upload_security import UploadSecurityError from modules.upload_security import UploadSecurityError
from modules.host_sandbox_policy import ( from modules.host_sandbox_policy import load_policy, save_policy
load_policy,
save_policy,
get_macos_deny_read_paths,
get_macos_deny_read_regexes,
)
from modules.user_manager import UserWorkspace from modules.user_manager import UserWorkspace
from core.web_terminal import WebTerminal from core.web_terminal import WebTerminal
from config.model_profiles import get_model_context_window from config.model_profiles import get_model_context_window
@ -54,30 +49,8 @@ from server.state import tool_approval_manager, user_question_manager
from server.extensions import socketio from server.extensions import socketio
from server.monitor import get_cached_monitor_snapshot from server.monitor import get_cached_monitor_snapshot
from server.files import sanitize_filename_preserve_unicode from server.files import sanitize_filename_preserve_unicode
import os
import re
UPLOAD_FOLDER_NAME = ".agents/user_upload" UPLOAD_FOLDER_NAME = ".agents/user_upload"
def _path_conflicts_with_deny_list(path: str) -> Optional[str]:
"""检查用户授权路径是否与内置 deny 列表冲突,返回错误信息或 None。"""
if not path:
return None
expanded = os.path.abspath(os.path.expanduser(path))
for deny_path in get_macos_deny_read_paths():
deny_expanded = os.path.abspath(os.path.expanduser(deny_path))
deny_lower = deny_expanded.lower().rstrip("/")
expanded_lower = expanded.lower().rstrip("/")
if expanded_lower == deny_lower or expanded_lower.startswith(deny_lower + "/"):
return f"禁止授权敏感路径: {path}"
for pattern in get_macos_deny_read_regexes():
try:
if re.search(pattern, expanded) or re.search(pattern, path):
return f"禁止授权敏感文件: {path}"
except re.error:
continue
return None
@chat_bp.route('/api/permission-mode', methods=['GET']) @chat_bp.route('/api/permission-mode', methods=['GET'])
@api_login_required @api_login_required
@with_terminal @with_terminal
@ -364,8 +337,6 @@ def get_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, user
"enabled": can_manage, "enabled": can_manage,
"writable_paths": data.get("macos_writable_paths", []), "writable_paths": data.get("macos_writable_paths", []),
"readable_extra_paths": data.get("macos_readable_extra_paths", []), "readable_extra_paths": data.get("macos_readable_extra_paths", []),
"deny_read_paths": data.get("macos_deny_read_paths", []),
"deny_read_regexes": data.get("macos_deny_read_regexes", []),
}) })
@chat_bp.route('/api/path-authorization', methods=['POST']) @chat_bp.route('/api/path-authorization', methods=['POST'])
@ -386,10 +357,11 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()] readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
if "/" in writable or "/" in readable_extra: if "/" in writable or "/" in readable_extra:
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400 return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
blocked_prefix = ("~/.ssh", "~/.gnupg", "~/.aws")
for p in writable + readable_extra: for p in writable + readable_extra:
conflict = _path_conflicts_with_deny_list(p) lowered = p.lower()
if conflict: if any(lowered.startswith(prefix) for prefix in blocked_prefix):
return jsonify({"success": False, "error": conflict}), 400 return jsonify({"success": False, "error": f"禁止授权敏感目录: {p}"}), 400
payload = save_policy({ payload = save_policy({
"macos_writable_paths": writable, "macos_writable_paths": writable,
"macos_readable_extra_paths": readable_extra "macos_readable_extra_paths": readable_extra
@ -398,6 +370,4 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
"success": True, "success": True,
"writable_paths": payload.get("macos_writable_paths", []), "writable_paths": payload.get("macos_writable_paths", []),
"readable_extra_paths": payload.get("macos_readable_extra_paths", []), "readable_extra_paths": payload.get("macos_readable_extra_paths", []),
"deny_read_paths": payload.get("macos_deny_read_paths", []),
"deny_read_regexes": payload.get("macos_deny_read_regexes", []),
}) })

View File

@ -431,14 +431,6 @@ def _record_hidden_versioning_checkpoint_after_run(
debug_log(f"[Versioning] 记录快照失败: {exc}") debug_log(f"[Versioning] 记录快照失败: {exc}")
except Exception as exc: except Exception as exc:
debug_log(f"[Versioning] 记录快照异常: {exc}") debug_log(f"[Versioning] 记录快照异常: {exc}")
finally:
# 当前输入处理结束,清除浅备份消息 ID避免后续无消息上下文时的错误归属。
try:
ctx = getattr(web_terminal, "context_manager", None)
if ctx is not None and hasattr(ctx, "current_shallow_message_id"):
ctx.current_shallow_message_id = None
except Exception:
pass
def _persist_and_echo_preceding_notice( def _persist_and_echo_preceding_notice(
@ -1018,13 +1010,6 @@ async def handle_task_with_sender(
videos=videos, videos=videos,
metadata=user_message_metadata metadata=user_message_metadata
) )
# 为浅备份 track_edit 提供当前用户消息 ID避免文件修改被归到上一个输入。
try:
web_terminal.context_manager.current_shallow_message_id = (
saved_user_message.get("message_id") if isinstance(saved_user_message, dict) else None
)
except Exception:
web_terminal.context_manager.current_shallow_message_id = None
try: try:
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1 user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
except Exception: except Exception:

View File

@ -1074,16 +1074,14 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
shallow_message_id = snapshot_data.get("shallow_message_id") shallow_message_id = snapshot_data.get("shallow_message_id")
except Exception: except Exception:
pass pass
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id}") files = row.get("files") or []
if shallow_message_id: debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id} files={files}")
if shallow_message_id and isinstance(files, list):
shallow_manager = ShallowVersioningManager( shallow_manager = ShallowVersioningManager(
project_path=workspace.project_path, project_path=workspace.project_path,
data_dir=workspace.data_dir, data_dir=workspace.data_dir,
conversation_id=normalized_id, conversation_id=normalized_id,
) )
# 与列表接口保持一致:从 shallow manager 重新计算文件变更,
# 而不是只给 inputs.jsonl 中原有的 files 补 patch_lines。
files = shallow_manager.get_file_diff_stats(str(shallow_message_id)) or []
stats = shallow_manager.get_diff_stats(str(shallow_message_id)) or {} stats = shallow_manager.get_diff_stats(str(shallow_message_id)) or {}
normalized_files: List[Dict[str, Any]] = [] normalized_files: List[Dict[str, Any]] = []
for file_item in files: for file_item in files:
@ -1093,7 +1091,7 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
path = str(item.get("path") or "") path = str(item.get("path") or "")
if path: if path:
patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id)) patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id))
debug_log(f"[Versioning][Detail] path={path} patch_lines={len(patch.get('lines') or [])}") debug_log(f"[Versioning][Detail] path={path} patch={patch}")
item["patch_lines"] = patch.get("lines") or [] item["patch_lines"] = patch.get("lines") or []
item["patch_truncated"] = bool(patch.get("truncated")) item["patch_truncated"] = bool(patch.get("truncated"))
normalized_files.append(item) normalized_files.append(item)

View File

@ -30,7 +30,7 @@
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg> </svg>
<span>Astrion</span> <span>AI Assistant</span>
</div> </div>
<!-- 等待动画 --> <!-- 等待动画 -->

View File

@ -30,7 +30,7 @@
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg> </svg>
<span>Astrion</span> <span>AI Assistant</span>
</div> </div>
<!-- 等待动画 --> <!-- 等待动画 -->

View File

@ -68,7 +68,7 @@
</head> </head>
<body> <body>
<div class="sidebar"> <div class="sidebar">
<div class="logo">Astrion <span>v5.3</span></div> <div class="logo">AI Agent <span>v5.3</span></div>
<div class="nav-item active">📁 项目文件</div> <div class="nav-item active">📁 项目文件</div>
<div class="nav-item">⚙️ 管理面板</div> <div class="nav-item">⚙️ 管理面板</div>
<div style="margin-top:20px; font-size:12px; color:#9CA3AF; font-weight:600;">TEMP</div> <div style="margin-top:20px; font-size:12px; color:#9CA3AF; font-weight:600;">TEMP</div>

View File

@ -133,7 +133,7 @@
</head> </head>
<body> <body>
<div class="sidebar"> <div class="sidebar">
<div class="logo">Astrion</div> <div class="logo">AI Agent</div>
<div class="nav-item active">Project Files</div> <div class="nav-item active">Project Files</div>
<div class="nav-item">Dashboard</div> <div class="nav-item">Dashboard</div>
<div style="flex:1"></div> <div style="flex:1"></div>

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Astrion</title> <title>AI Agent System</title>
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/easter-eggs/flood.css"> <link rel="stylesheet" href="/static/easter-eggs/flood.css">
<link rel="stylesheet" href="/static/easter-eggs/snake.css"> <link rel="stylesheet" href="/static/easter-eggs/snake.css">

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>登录 - Astrion</title> <title>登录 - AI Agent</title>
<script> <script>
(function () { (function () {
try { try {

View File

@ -255,7 +255,7 @@
</div> </div>
<div class="sheet-body"> <div class="sheet-body">
<div class="section-card status-card"> <div class="section-card status-card">
<strong>Astrion v2.4</strong> <strong>AI Agent v2.4</strong>
<p>已连接 · 思考模式</p> <p>已连接 · 思考模式</p>
</div> </div>
<div class="section-card"> <div class="section-card">

View File

@ -335,7 +335,7 @@
</div> </div>
<div class="sheet-body"> <div class="sheet-body">
<div class="section-card"> <div class="section-card">
<strong>Astrion v2.4</strong> <strong>AI Agent v2.4</strong>
<p>思考模式 · 已连接</p> <p>思考模式 · 已连接</p>
</div> </div>
<div class="section-card"> <div class="section-card">

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>注册 - Astrion</title> <title>注册 - AI Agent</title>
<script> <script>
(function () { (function () {
try { try {

View File

@ -1,7 +1,7 @@
<template> <template>
<main class="auth-page"> <main class="auth-page">
<section class="auth-card"> <section class="auth-card">
<h1 class="auth-title">Astrion 登录</h1> <h1 class="auth-title">AI Agent 登录</h1>
<p class="auth-subtitle">使用账号继续访问工作台</p> <p class="auth-subtitle">使用账号继续访问工作台</p>
<div class="auth-form-group"> <div class="auth-form-group">

View File

@ -98,7 +98,7 @@
</template> </template>
</div> </div>
<div v-else-if="msg.role === 'assistant'" class="assistant-message"> <div v-else-if="msg.role === 'assistant'" class="assistant-message">
<!-- assistant 消息前显示 Astrion 头部 --> <!-- assistant 消息前显示 AI Assistant 头部 -->
<!-- 只有当前一条消息是 user 且当前消息有内容时才显示 --> <!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
<div <div
v-if=" v-if="
@ -704,9 +704,9 @@ const stackedBlocksEnabled = computed(() => {
}); });
const aiAssistantName = computed(() => { const aiAssistantName = computed(() => {
if (!personalization.form.use_custom_names) { if (!personalization.form.use_custom_names) {
return 'Astrion'; return 'AI Assistant';
} }
return personalization.form.self_identify || 'Astrion'; return personalization.form.self_identify || 'AI Assistant';
}); });
const userName = computed(() => { const userName = computed(() => {
if (!personalization.form.use_custom_names) { if (!personalization.form.use_custom_names) {

View File

@ -24,22 +24,11 @@
仅可读 仅可读
</button> </button>
</div> </div>
<p class="hint">
{{
mode === 'writable'
? '可读可写路径在 workspace-write 沙箱中可写入,只读沙箱中仅可读。'
: '仅可读路径在只读沙箱中会被加入允许读取列表;工作区可写沙箱默认已可读。'
}}
</p>
<textarea <textarea
class="path-input" class="path-input"
:value="value" :value="value"
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)" @input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
:placeholder=" placeholder="每行一个路径,例如:~/Desktop/agents-export"
mode === 'writable'
? '每行一个路径,例如:~/Desktop/agents-export'
: '每行一个路径,例如:~/Documents/reference'
"
/> />
<div class="actions"> <div class="actions">
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button> <button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>

View File

@ -10,7 +10,7 @@
aria-hidden="true" aria-hidden="true"
></span> ></span>
<div class="brand-text"> <div class="brand-text">
<span class="brand-name">Astrion</span> <span class="brand-name">AI Agent</span>
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span> <span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
</div> </div>
</div> </div>

View File

@ -28,7 +28,7 @@ const TUTORIAL_VERSION = 'v1';
const DEFAULT_STEPS: TutorialStep[] = [ const DEFAULT_STEPS: TutorialStep[] = [
{ {
id: 'welcome', id: 'welcome',
title: '欢迎使用 Astrion', title: '欢迎使用 AI Agent 系统',
description: '我们将用 2-3 分钟带你快速了解核心功能。', description: '我们将用 2-3 分钟带你快速了解核心功能。',
target: null, target: null,
mode: 'info', mode: 'info',

View File

@ -402,7 +402,7 @@
</div> </div>
</div> </div>
<div class="status-right"> <div class="status-right">
<span style="opacity: 0.6;">Astrion Terminal Monitor v1.0</span> <span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
</div> </div>
</div> </div>
@ -555,7 +555,7 @@
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m'); term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m'); term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
term.writeln(''); term.writeln('');
term.writeln('\x1b[33m正在连接到 Astrion...\x1b[0m'); term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
term.writeln(''); term.writeln('');
} }