Compare commits
4 Commits
2264a86851
...
6a2cb820bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a2cb820bc | |||
| 765196ea3c | |||
| 3db4e0beb4 | |||
| 15106e7d00 |
48
.agents/memory/astrion_naming.md
Normal file
48
.agents/memory/astrion_naming.md
Normal file
@ -0,0 +1,48 @@
|
||||
---
|
||||
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`
|
||||
10
AGENTS.md
10
AGENTS.md
@ -7,6 +7,16 @@
|
||||
|
||||
> 和用户交流时默认使用中文。
|
||||
|
||||
## 0) 项目名称
|
||||
|
||||
本项目(仓库 `agents`)的暂定产品名为 **Astrion**。
|
||||
|
||||
- **构词**:`Astr-`(希腊语 *astron*,星星/天体)+ `-ion`(粒子/状态后缀,如 photon、electron)。
|
||||
- **意象**:星之子、星际粒子、来自宇宙尘埃的微小发光体。
|
||||
- **寓意**:Agent 像星际间传递信号的粒子,把人类意图传递到工具、文件、终端与子智能体之间;同时把零散任务像星尘聚合成恒星一样,聚合成可执行结果。
|
||||
- **状态**:暂定名,后续若变更需同步更新 `AGENTS.md`、`claude.md` 与项目记忆 `astrion_naming`。
|
||||
- **中文参考**:阿斯特里恩;简称可考虑 **星子**。
|
||||
|
||||
## 1) 当前项目结构(按代码现状)
|
||||
|
||||
- **后端入口**
|
||||
|
||||
@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Overview
|
||||
|
||||
多用户 AI Agent 系统,为每个登录用户提供独立的 Docker 容器环境。支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
||||
多用户 AI Agent 系统,产品暂定名为 **Astrion**(`Astr-` 星星/天体 + `-ion` 粒子后缀,意象为“星之子、星际粒子”)。为每个登录用户提供独立的 Docker 容器环境,支持终端交互、文件操作、对话管理和实时监控。主要用于学习和实验"智能体 + 真实 Dev Workflow",代码大量由 AI 生成。
|
||||
|
||||
## Core Architecture
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# AI Agent 系统
|
||||
# Astrion
|
||||
|
||||
一个功能完整的 AI 智能体系统,支持多模态交互、子智能体协作、实时终端操作和丰富的工具集成。
|
||||
|
||||
|
||||
@ -1,15 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@ -97,7 +97,7 @@ function isWideCodePoint(code: number): boolean {
|
||||
export function WelcomePanel({ status }: { status: CliStatus }) {
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text color="gray">Agents</Text>
|
||||
<Text color="gray">Astrion</Text>
|
||||
<Box borderStyle="round" borderColor="gray" paddingX={2} paddingY={1} flexDirection="column">
|
||||
<Text bold>Welcome back!</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 }) {
|
||||
return (
|
||||
<Box borderStyle="round" borderColor="gray" paddingX={1} flexDirection="column" marginTop={1}>
|
||||
<Text>{'>_ Agents CLI'}</Text>
|
||||
<Text>{'>_ Astrion CLI'}</Text>
|
||||
<Text> </Text>
|
||||
<StatusField label="Model" value={status.model} />
|
||||
<StatusField label="Directory" value={shortPath(status.directory)} />
|
||||
|
||||
@ -1,4 +1,28 @@
|
||||
{
|
||||
"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(\\.[^/]*)?$"
|
||||
]
|
||||
}
|
||||
|
||||
@ -105,6 +105,9 @@ USER_SPACE_DIR = _runtime_dir("USER_SPACE_DIR", "users")
|
||||
# API 专用用户与工作区(与网页用户隔离)
|
||||
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"
|
||||
|
||||
@ -190,6 +193,7 @@ __all__ = [
|
||||
"INVITE_CODES_FILE",
|
||||
"ADMIN_POLICY_FILE",
|
||||
"API_USER_SPACE_DIR",
|
||||
"CUSTOM_SKILLS_DIR",
|
||||
"API_USERS_DB_FILE",
|
||||
"API_TOKENS_FILE",
|
||||
"API_USAGE_FILE",
|
||||
|
||||
@ -112,13 +112,13 @@ class MessagesMixin:
|
||||
# 根据当前模型多模态能力选择系统提示。system prompt 按对话冻结;
|
||||
# 工具 schema 保持动态,由 define_tools 每轮根据真实状态生成。
|
||||
current_model = getattr(self, "model_key", None)
|
||||
prompt_name = "main_system_qwenvl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
|
||||
prompt_name = "main_system_vl" if (model_supports_image(current_model) or model_supports_video(current_model)) else "main_system"
|
||||
model_key = getattr(self, "model_key", None)
|
||||
prompt_replacements = get_model_prompt_replacements(model_key)
|
||||
|
||||
def _build_main_system_prompt() -> str:
|
||||
system_prompt_template = self.load_prompt(prompt_name)
|
||||
# main_system.txt / main_system_qwenvl.txt 仅使用 {model_description}
|
||||
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
|
||||
return system_prompt_template.format(
|
||||
model_description=prompt_replacements.get("model_description", "")
|
||||
)
|
||||
|
||||
@ -125,4 +125,4 @@ class PromptMixin:
|
||||
if prompt_file.exists():
|
||||
with open(prompt_file, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
return "你是一个AI助手。"
|
||||
return "你是 Astrion,一个智能助手。"
|
||||
|
||||
@ -26,7 +26,6 @@ try:
|
||||
READONLY_RUN_COMMAND_ALLOWED,
|
||||
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
|
||||
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
|
||||
AGENT_SKILLS_DIR,
|
||||
)
|
||||
except ImportError:
|
||||
import sys
|
||||
@ -50,7 +49,6 @@ except ImportError:
|
||||
READONLY_RUN_COMMAND_ALLOWED,
|
||||
READONLY_RUN_COMMAND_ALLOWED_GIT_SUBCOMMANDS,
|
||||
READONLY_RUN_COMMAND_BLOCKED_TOKENS,
|
||||
AGENT_SKILLS_DIR,
|
||||
)
|
||||
|
||||
from modules.file_manager import FileManager
|
||||
@ -496,9 +494,6 @@ class MainTerminalToolsExecutionMixin:
|
||||
return (project_root / candidate).resolve()
|
||||
|
||||
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)
|
||||
if inferred:
|
||||
return inferred
|
||||
@ -641,9 +636,12 @@ class MainTerminalToolsExecutionMixin:
|
||||
def _track_shallow_versioning(self, file_path: Any) -> None:
|
||||
"""Track a file edit for shallow versioning (conversation-scoped undo)."""
|
||||
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
|
||||
perf_log("_track_shallow_versioning called", extra={
|
||||
"conversation_id": conversation_id,
|
||||
"message_id": message_id,
|
||||
"project_path": str(self.project_path),
|
||||
"data_dir": str(self.data_dir),
|
||||
"file_path": str(file_path),
|
||||
@ -657,9 +655,10 @@ class MainTerminalToolsExecutionMixin:
|
||||
data_dir=self.data_dir,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
manager.track_edit(file_path, conversation_id)
|
||||
manager.track_edit(file_path, message_id)
|
||||
perf_log("_track_shallow_versioning done", extra={
|
||||
"conversation_id": conversation_id,
|
||||
"message_id": message_id,
|
||||
"file_path": str(file_path),
|
||||
"tracked_files_count": len(getattr(manager, "_tracked_files", set())),
|
||||
})
|
||||
|
||||
2
main.py
2
main.py
@ -27,7 +27,7 @@ class AgentSystem:
|
||||
async def initialize(self):
|
||||
"""初始化系统"""
|
||||
print("\n" + "="*50)
|
||||
print("ð¤ AI Agent 系统启动")
|
||||
print("ð¤ Astrion 启动")
|
||||
print("="*50)
|
||||
|
||||
# 1. 获取项目路径
|
||||
|
||||
@ -7,17 +7,55 @@ from typing import Dict, List
|
||||
|
||||
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()
|
||||
# 部署级配置(机器特定可读写路径,会被运行时写回)→ ~/.agents/<mode>/config
|
||||
_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:
|
||||
if _POLICY_PATH.exists():
|
||||
return
|
||||
_POLICY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
_POLICY_PATH.write_text(
|
||||
json.dumps({"macos_writable_paths": [], "macos_readable_extra_paths": []}, ensure_ascii=False, indent=2),
|
||||
json.dumps(_default_policy(), ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
@ -28,30 +66,38 @@ def load_policy() -> Dict:
|
||||
try:
|
||||
data = json.loads(_POLICY_PATH.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
|
||||
data = _default_policy()
|
||||
if not isinstance(data, dict):
|
||||
data = {"macos_writable_paths": [], "macos_readable_extra_paths": []}
|
||||
writable_items = data.get("macos_writable_paths")
|
||||
if not isinstance(writable_items, list):
|
||||
writable_items = []
|
||||
readable_items = data.get("macos_readable_extra_paths")
|
||||
if not isinstance(readable_items, list):
|
||||
readable_items = []
|
||||
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 = _default_policy()
|
||||
|
||||
defaults = _default_policy()
|
||||
needs_save = False
|
||||
for key in defaults:
|
||||
if key not in data or not isinstance(data.get(key), list):
|
||||
data[key] = list(defaults[key])
|
||||
needs_save = True
|
||||
|
||||
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
|
||||
|
||||
|
||||
def save_policy(policy: Dict) -> Dict:
|
||||
payload = dict(policy or {})
|
||||
writable_items = payload.get("macos_writable_paths")
|
||||
if not isinstance(writable_items, list):
|
||||
writable_items = []
|
||||
readable_items = payload.get("macos_readable_extra_paths")
|
||||
if not isinstance(readable_items, list):
|
||||
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()]
|
||||
defaults = _default_policy()
|
||||
for key in defaults:
|
||||
items = payload.get(key)
|
||||
if not isinstance(items, list):
|
||||
items = list(defaults[key])
|
||||
payload[key] = [str(x).strip() for x in items if str(x).strip()]
|
||||
with _LOCK:
|
||||
_ensure_file()
|
||||
_POLICY_PATH.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
@ -77,3 +123,25 @@ def get_macos_readable_paths() -> List[str]:
|
||||
if val and val not in merged:
|
||||
merged.append(val)
|
||||
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
|
||||
|
||||
@ -7,7 +7,11 @@ import shlex
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
from modules.host_sandbox_policy import get_macos_writable_paths
|
||||
from modules.host_sandbox_policy import (
|
||||
get_macos_writable_paths,
|
||||
get_macos_deny_read_paths,
|
||||
get_macos_deny_read_regexes,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -22,6 +26,84 @@ class HostSandboxError(RuntimeError):
|
||||
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_FULL = "full" # 完全开放
|
||||
@ -131,13 +213,24 @@ def _build_macos_readonly_plan(
|
||||
if not sandbox_exec:
|
||||
raise HostSandboxError("macOS 未找到 sandbox-exec,拒绝执行宿主机命令。")
|
||||
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 = (
|
||||
'(version 1) '
|
||||
'(deny default) '
|
||||
'(allow sysctl-read) '
|
||||
'(allow process*) '
|
||||
'(version 1)\n'
|
||||
'(deny default)\n'
|
||||
'(allow sysctl-read)\n'
|
||||
'(allow process*)\n'
|
||||
f'{network_policy}'
|
||||
'(allow file-read*) '
|
||||
'(allow file-read*)\n'
|
||||
f'{deny_rules}\n'
|
||||
f'(allow file-read* (subpath "{workspace}"))\n'
|
||||
'(allow file-write* (literal "/dev/null"))'
|
||||
)
|
||||
cmd = [sandbox_exec, "-p", profile, "/bin/bash", "-lc", command]
|
||||
@ -178,13 +271,20 @@ def _macos_profile_for_workspace(
|
||||
write_rules.append(f'(subpath "{entry}")')
|
||||
write_expr = " ".join(write_rules)
|
||||
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 (
|
||||
'(version 1) '
|
||||
'(deny default) '
|
||||
'(allow sysctl-read) '
|
||||
'(allow process*) '
|
||||
'(version 1)\n'
|
||||
'(deny default)\n'
|
||||
'(allow sysctl-read)\n'
|
||||
'(allow process*)\n'
|
||||
f'{network_policy}'
|
||||
'(allow file-read*) '
|
||||
'(allow file-read*)\n'
|
||||
f'{deny_rules}\n'
|
||||
f'(allow file-read* (subpath "{workspace}"))\n'
|
||||
f'(allow file-write* {write_expr})'
|
||||
)
|
||||
|
||||
|
||||
@ -189,6 +189,14 @@ class ShallowVersioningManager:
|
||||
# ----------------------------
|
||||
# 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:
|
||||
"""Track a file before it is edited; backup its current content if not already tracked."""
|
||||
tracking_path = self._normalize_path(file_path)
|
||||
@ -206,12 +214,13 @@ class ShallowVersioningManager:
|
||||
backup = self._create_backup(resolved, 1)
|
||||
self._tracked_files.add(tracking_path)
|
||||
|
||||
# Add to the most recent snapshot if one exists, otherwise create a pre-edit snapshot.
|
||||
# 归属到当前 message_id 对应的最新 snapshot;不存在则新建。
|
||||
# 避免把本次输入的修改追加到上一个输入的 snapshot。
|
||||
now = datetime.now().isoformat()
|
||||
if self._snapshots:
|
||||
latest = self._snapshots[-1]
|
||||
latest.tracked_file_backups[tracking_path] = backup
|
||||
self._persist_snapshot(latest)
|
||||
target_snapshot = self._find_latest_snapshot_by_message_id(message_id)
|
||||
if target_snapshot is not None:
|
||||
target_snapshot.tracked_file_backups[tracking_path] = backup
|
||||
self._persist_snapshot(target_snapshot)
|
||||
else:
|
||||
snapshot = ShallowSnapshot(
|
||||
message_id=message_id,
|
||||
@ -256,8 +265,17 @@ class ShallowVersioningManager:
|
||||
tracked_file_backups=tracked_file_backups,
|
||||
timestamp=now,
|
||||
)
|
||||
self._snapshots.append(snapshot)
|
||||
self._snapshot_sequence += 1
|
||||
# 如果当前 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._snapshot_sequence += 1
|
||||
self._persist_snapshot(snapshot)
|
||||
|
||||
# Evict old snapshots while keeping the most recent MAX_SNAPSHOTS.
|
||||
|
||||
@ -11,7 +11,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Sequence
|
||||
|
||||
from config import AGENT_SKILLS_DIR, WORKSPACE_SKILLS_DIRNAME
|
||||
from config import AGENT_SKILLS_DIR, CUSTOM_SKILLS_DIR, IS_HOST_MODE, WORKSPACE_SKILLS_DIRNAME
|
||||
from utils.logger import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
@ -48,10 +48,15 @@ def ensure_workspace_skills_dir(project_path: str | Path) -> 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:
|
||||
return None
|
||||
try:
|
||||
if IS_HOST_MODE:
|
||||
return Path(CUSTOM_SKILLS_DIR).expanduser().resolve()
|
||||
data_path = Path(data_dir).expanduser().resolve()
|
||||
if data_path.name == "data" and data_path.parent.parent.name == "workspaces":
|
||||
return (data_path.parent.parent.parent / "agentskills").resolve()
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "ai-agent-frontend",
|
||||
"name": "astrion-frontend",
|
||||
"version": "4.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
你是 Astrion,一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
|
||||
|
||||
{model_description}
|
||||
|
||||
你是一名运行在云端服务器/用户电脑(根据工作区信息判断)根据上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
||||
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
||||
|
||||
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
|
||||
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
你是 Astrion,一个运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。
|
||||
|
||||
{model_description}
|
||||
|
||||
你是一名运行在云端服务器/用户电脑(根据工作区信息判断)上的全能型智能助手,专为**普通用户和开发者**设计。你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
||||
你的用户可能是没有编程背景的业务人员,也可能是专业开发者——你需要根据对话上下文灵活调整表达方式:
|
||||
|
||||
- **对普通用户**:使用通俗易懂的语言,避免技术黑话,主动解释操作步骤
|
||||
- **对开发者**:可以使用专业术语,提供技术细节和最佳实践建议
|
||||
@ -207,7 +207,7 @@ def _write_custom_models(target_path: Path, entry: dict) -> Path | None:
|
||||
|
||||
def run_wizard(force: bool = False) -> int:
|
||||
print("=" * 60)
|
||||
print(" AI Agent 首次启动初始化向导")
|
||||
print(" Astrion 首次启动初始化向导")
|
||||
print("=" * 60)
|
||||
print("逐项设置启动前必须确定的内容,每项可回车使用 [默认值]。")
|
||||
print()
|
||||
@ -311,7 +311,7 @@ def run_wizard(force: bool = False) -> int:
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="AI Agent 首次启动初始化向导")
|
||||
parser = argparse.ArgumentParser(description="Astrion 首次启动初始化向导")
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
|
||||
@ -35,7 +35,12 @@ from modules.skills_manager import (
|
||||
sync_workspace_skills,
|
||||
)
|
||||
from modules.upload_security import UploadSecurityError
|
||||
from modules.host_sandbox_policy import load_policy, save_policy
|
||||
from modules.host_sandbox_policy import (
|
||||
load_policy,
|
||||
save_policy,
|
||||
get_macos_deny_read_paths,
|
||||
get_macos_deny_read_regexes,
|
||||
)
|
||||
from modules.user_manager import UserWorkspace
|
||||
from core.web_terminal import WebTerminal
|
||||
from config.model_profiles import get_model_context_window
|
||||
@ -49,8 +54,30 @@ from server.state import tool_approval_manager, user_question_manager
|
||||
from server.extensions import socketio
|
||||
from server.monitor import get_cached_monitor_snapshot
|
||||
from server.files import sanitize_filename_preserve_unicode
|
||||
import os
|
||||
import re
|
||||
|
||||
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'])
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
@ -337,6 +364,8 @@ def get_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, user
|
||||
"enabled": can_manage,
|
||||
"writable_paths": data.get("macos_writable_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'])
|
||||
@ -357,11 +386,10 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
|
||||
readable_extra = [str(x).strip() for x in readable_items if str(x).strip()]
|
||||
if "/" in writable or "/" in readable_extra:
|
||||
return jsonify({"success": False, "error": "禁止授权根目录 /"}), 400
|
||||
blocked_prefix = ("~/.ssh", "~/.gnupg", "~/.aws")
|
||||
for p in writable + readable_extra:
|
||||
lowered = p.lower()
|
||||
if any(lowered.startswith(prefix) for prefix in blocked_prefix):
|
||||
return jsonify({"success": False, "error": f"禁止授权敏感目录: {p}"}), 400
|
||||
conflict = _path_conflicts_with_deny_list(p)
|
||||
if conflict:
|
||||
return jsonify({"success": False, "error": conflict}), 400
|
||||
payload = save_policy({
|
||||
"macos_writable_paths": writable,
|
||||
"macos_readable_extra_paths": readable_extra
|
||||
@ -370,4 +398,6 @@ def update_path_authorization(terminal: WebTerminal, workspace: UserWorkspace, u
|
||||
"success": True,
|
||||
"writable_paths": payload.get("macos_writable_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", []),
|
||||
})
|
||||
|
||||
@ -431,6 +431,14 @@ def _record_hidden_versioning_checkpoint_after_run(
|
||||
debug_log(f"[Versioning] 记录快照失败: {exc}")
|
||||
except Exception as 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(
|
||||
@ -1010,6 +1018,13 @@ async def handle_task_with_sender(
|
||||
videos=videos,
|
||||
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:
|
||||
user_message_index = len(getattr(web_terminal.context_manager, "conversation_history", []) or []) - 1
|
||||
except Exception:
|
||||
|
||||
@ -1074,14 +1074,16 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
|
||||
shallow_message_id = snapshot_data.get("shallow_message_id")
|
||||
except Exception:
|
||||
pass
|
||||
files = row.get("files") or []
|
||||
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id} files={files}")
|
||||
if shallow_message_id and isinstance(files, list):
|
||||
debug_log(f"[Versioning][Detail] resolved shallow_message_id={shallow_message_id}")
|
||||
if shallow_message_id:
|
||||
shallow_manager = ShallowVersioningManager(
|
||||
project_path=workspace.project_path,
|
||||
data_dir=workspace.data_dir,
|
||||
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 {}
|
||||
normalized_files: List[Dict[str, Any]] = []
|
||||
for file_item in files:
|
||||
@ -1091,7 +1093,7 @@ def get_conversation_versioning_checkpoint_detail(conversation_id, seq: int, ter
|
||||
path = str(item.get("path") or "")
|
||||
if path:
|
||||
patch = shallow_manager.get_file_patch_lines(path, str(shallow_message_id))
|
||||
debug_log(f"[Versioning][Detail] path={path} patch={patch}")
|
||||
debug_log(f"[Versioning][Detail] path={path} patch_lines={len(patch.get('lines') or [])}")
|
||||
item["patch_lines"] = patch.get("lines") or []
|
||||
item["patch_truncated"] = bool(patch.get("truncated"))
|
||||
normalized_files.append(item)
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<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>
|
||||
</svg>
|
||||
<span>AI Assistant</span>
|
||||
<span>Astrion</span>
|
||||
</div>
|
||||
|
||||
<!-- 等待动画 -->
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
<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>
|
||||
</svg>
|
||||
<span>AI Assistant</span>
|
||||
<span>Astrion</span>
|
||||
</div>
|
||||
|
||||
<!-- 等待动画 -->
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<div class="logo">AI Agent <span>v5.3</span></div>
|
||||
<div class="logo">Astrion <span>v5.3</span></div>
|
||||
<div class="nav-item active">📁 项目文件</div>
|
||||
<div class="nav-item">⚙️ 管理面板</div>
|
||||
<div style="margin-top:20px; font-size:12px; color:#9CA3AF; font-weight:600;">TEMP</div>
|
||||
|
||||
@ -133,7 +133,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="sidebar">
|
||||
<div class="logo">AI Agent ✨</div>
|
||||
<div class="logo">Astrion ✨</div>
|
||||
<div class="nav-item active">Project Files</div>
|
||||
<div class="nav-item">Dashboard</div>
|
||||
<div style="flex:1"></div>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AI Agent System</title>
|
||||
<title>Astrion</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="stylesheet" href="/static/easter-eggs/flood.css">
|
||||
<link rel="stylesheet" href="/static/easter-eggs/snake.css">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录 - AI Agent</title>
|
||||
<title>登录 - Astrion</title>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@ -255,7 +255,7 @@
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="section-card status-card">
|
||||
<strong>AI Agent v2.4</strong>
|
||||
<strong>Astrion v2.4</strong>
|
||||
<p>已连接 · 思考模式</p>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
|
||||
@ -335,7 +335,7 @@
|
||||
</div>
|
||||
<div class="sheet-body">
|
||||
<div class="section-card">
|
||||
<strong>AI Agent v2.4</strong>
|
||||
<strong>Astrion v2.4</strong>
|
||||
<p>思考模式 · 已连接</p>
|
||||
</div>
|
||||
<div class="section-card">
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>注册 - AI Agent</title>
|
||||
<title>注册 - Astrion</title>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<main class="auth-page">
|
||||
<section class="auth-card">
|
||||
<h1 class="auth-title">AI Agent 登录</h1>
|
||||
<h1 class="auth-title">Astrion 登录</h1>
|
||||
<p class="auth-subtitle">使用账号继续访问工作台</p>
|
||||
|
||||
<div class="auth-form-group">
|
||||
|
||||
@ -98,7 +98,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="msg.role === 'assistant'" class="assistant-message">
|
||||
<!-- 在 assistant 消息前显示 AI Assistant 头部 -->
|
||||
<!-- 在 assistant 消息前显示 Astrion 头部 -->
|
||||
<!-- 只有当前一条消息是 user 且当前消息有内容时才显示 -->
|
||||
<div
|
||||
v-if="
|
||||
@ -704,9 +704,9 @@ const stackedBlocksEnabled = computed(() => {
|
||||
});
|
||||
const aiAssistantName = computed(() => {
|
||||
if (!personalization.form.use_custom_names) {
|
||||
return 'AI Assistant';
|
||||
return 'Astrion';
|
||||
}
|
||||
return personalization.form.self_identify || 'AI Assistant';
|
||||
return personalization.form.self_identify || 'Astrion';
|
||||
});
|
||||
const userName = computed(() => {
|
||||
if (!personalization.form.use_custom_names) {
|
||||
|
||||
@ -24,11 +24,22 @@
|
||||
仅可读
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
<p class="hint">
|
||||
{{
|
||||
mode === 'writable'
|
||||
? '可读可写路径在 workspace-write 沙箱中可写入,只读沙箱中仅可读。'
|
||||
: '仅可读路径在只读沙箱中会被加入允许读取列表;工作区可写沙箱默认已可读。'
|
||||
}}
|
||||
</p>
|
||||
<textarea
|
||||
class="path-input"
|
||||
:value="value"
|
||||
@input="$emit('update:value', ($event.target as HTMLTextAreaElement).value)"
|
||||
placeholder="每行一个路径,例如:~/Desktop/agents-export"
|
||||
:placeholder="
|
||||
mode === 'writable'
|
||||
? '每行一个路径,例如:~/Desktop/agents-export'
|
||||
: '每行一个路径,例如:~/Documents/reference'
|
||||
"
|
||||
/>
|
||||
<div class="actions">
|
||||
<button type="button" class="btn" @click="$emit('save')" :disabled="saving">保存</button>
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div class="brand-text">
|
||||
<span class="brand-name">AI Agent</span>
|
||||
<span class="brand-name">Astrion</span>
|
||||
<span class="agent-version" v-if="agentVersion">{{ agentVersion }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -28,7 +28,7 @@ const TUTORIAL_VERSION = 'v1';
|
||||
const DEFAULT_STEPS: TutorialStep[] = [
|
||||
{
|
||||
id: 'welcome',
|
||||
title: '欢迎使用 AI Agent 系统',
|
||||
title: '欢迎使用 Astrion',
|
||||
description: '我们将用 2-3 分钟带你快速了解核心功能。',
|
||||
target: null,
|
||||
mode: 'info',
|
||||
|
||||
@ -402,7 +402,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
<span style="opacity: 0.6;">AI Agent Terminal Monitor v1.0</span>
|
||||
<span style="opacity: 0.6;">Astrion Terminal Monitor v1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -555,7 +555,7 @@
|
||||
term.writeln('\x1b[1;36m║ 🤖 AI Terminal Monitor v1.0 ║\x1b[0m');
|
||||
term.writeln('\x1b[1;36m╚════════════════════════════════════════╝\x1b[0m');
|
||||
term.writeln('');
|
||||
term.writeln('\x1b[33m正在连接到AI Agent...\x1b[0m');
|
||||
term.writeln('\x1b[33m正在连接到 Astrion...\x1b[0m');
|
||||
term.writeln('');
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user